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

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.
@@ -144,7 +144,7 @@ const ofRelease = (release, buffer) => (release.buffers ? release.buffers.has(bu
144
144
  // The memory and the open files of a VM whose threads all returned
145
145
  // (erts_wasm_hibernate), as snapshot.bin. The files that the boot wrote
146
146
  // are the ones that are not views of release.bin (unpack: canOwn).
147
- function capture(m, release, listeners, bootPoint = false, skip = () => false) {
147
+ function capture(m, release, listeners, bootPoint = false, skip = () => false, flags = '') {
148
148
  const PAGE = 65536;
149
149
  const heap = m.HEAPU8;
150
150
  const words = new BigUint64Array(heap.buffer, 0, heap.length / 8);
@@ -187,6 +187,9 @@ function capture(m, release, listeners, bootPoint = false, skip = () => false) {
187
187
  // The release of this memory (snapshot_key of .release.json): a VM of
188
188
  // another release does not restore it.
189
189
  release: releaseMeta(release).snapshot_key ?? null,
190
+ // The flags of the emulator of this memory (BEAM_ERL_FLAGS): a VM with
191
+ // other flags does not restore it (-Mea min changes the allocators).
192
+ flags,
190
193
  nifs: libs && { count: libs.count, libs: libs.libs.map(({ data, ...l }) => l) },
191
194
  }));
192
195
  const out = new Uint8Array(12 + head.length + (pages.length + libPages) * PAGE);
@@ -219,18 +222,69 @@ async function loadSnapshot(env) {
219
222
 
220
223
  // The header of a snapshot, without its pages: boot_point, release, ...
221
224
  export function snapshotHeader(bytes) {
222
- const { pagesData, ...head } = parseSnapshot(bytes);
225
+ const { pagesData, packed, ...head } = parseSnapshot(bytes);
223
226
  return head;
224
227
  }
225
228
 
226
- function parseSnapshot(bytes) {
229
+ // A snapshot is "BEAMSNP1" (the pages as they are) or "BEAMSNZ1" (the
230
+ // pages in gzip, packSnapshot): the same header, which the host reads
231
+ // with no inflate. The pages of BEAMSNZ1 are in packed, and inflate()
232
+ // gives pagesData.
233
+ export function parseSnapshot(bytes) {
227
234
  const b = new Uint8Array(bytes);
228
- if (new TextDecoder().decode(b.subarray(0, 8)) !== 'BEAMSNP1') throw new Error('snapshot.bin: not a snapshot');
235
+ const magic = new TextDecoder().decode(b.subarray(0, 8));
236
+ if (magic !== 'BEAMSNP1' && magic !== 'BEAMSNZ1') throw new Error('snapshot.bin: not a snapshot');
229
237
  const len = new DataView(b.buffer, b.byteOffset).getUint32(8);
230
238
  const head = JSON.parse(new TextDecoder().decode(b.subarray(12, 12 + len)));
231
- return { ...head, pagesData: b.subarray(12 + len) };
239
+ return magic === 'BEAMSNP1' ? { ...head, pagesData: b.subarray(12 + len) }
240
+ : { ...head, pagesData: null, packed: b.subarray(12 + len) };
232
241
  }
233
242
 
243
+ // The bytes of the pages of a snapshot (the pages of the VM, then the
244
+ // pages of the NIF libraries).
245
+ const pagesSize = (head) => 65536 * (head.pages.length + (head.nifs?.libs ?? []).reduce((n, l) => n + l.pages.length, 0));
246
+
247
+ // A snapshot of BEAMSNP1 as BEAMSNZ1: the pages in gzip. A snapshot of
248
+ // the build is a module of the Worker, and the module stays in the memory
249
+ // of the isolate: the pages of a Phoenix app are about 21 MB, and 5 MB in
250
+ // gzip.
251
+ export async function packSnapshot(bytes) {
252
+ const b = new Uint8Array(bytes);
253
+ if (new TextDecoder().decode(b.subarray(0, 8)) !== 'BEAMSNP1') return b;
254
+ const len = new DataView(b.buffer, b.byteOffset).getUint32(8);
255
+ const gz = new Uint8Array(await new Response(new Blob([b.subarray(12 + len)]).stream()
256
+ .pipeThrough(new CompressionStream('gzip'))).arrayBuffer());
257
+ const out = new Uint8Array(12 + len + gz.length);
258
+ out.set(new TextEncoder().encode('BEAMSNZ1'));
259
+ out.set(b.subarray(8, 12 + len), 8);
260
+ out.set(gz, 12 + len);
261
+ return out;
262
+ }
263
+
264
+ // The pages of a parsed snapshot (pagesData), with no copy of the whole
265
+ // gzip output: the header gives their size.
266
+ export async function inflateSnapshot(snap) {
267
+ if (!snap.packed) return snap;
268
+ const size = pagesSize(snap);
269
+ const out = new Uint8Array(size);
270
+ const reader = new Blob([snap.packed]).stream().pipeThrough(new DecompressionStream('gzip')).getReader();
271
+ let at = 0;
272
+ for (;;) {
273
+ const { value, done } = await reader.read();
274
+ if (done) break;
275
+ if (at + value.length > size) throw new Error('snapshot: more pages than its header gives');
276
+ out.set(value, at);
277
+ at += value.length;
278
+ }
279
+ if (at !== size) throw new Error('snapshot: fewer pages than its header gives');
280
+ snap.pagesData = out;
281
+ snap.packed = null;
282
+ return snap;
283
+ }
284
+
285
+ // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
+ export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
+
234
288
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
235
289
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
236
290
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -383,7 +437,7 @@ function seed(m) {
383
437
  // most).
384
438
  const CHUNK = 1 << 20;
385
439
 
386
- class Persist {
440
+ export class Persist {
387
441
  constructor(sql, dirs) {
388
442
  this.sql = sql;
389
443
  this.dirs = dirs.map((d) => d.replace(/\/+$/, '')).filter((d) => d.startsWith('/') && d.length > 1);
@@ -429,6 +483,14 @@ class Persist {
429
483
  }
430
484
  }
431
485
 
486
+ // Each file that the VM wrote after the last save. The timer of a write
487
+ // calls it, and also the stop of the VM, so a stop loses no write.
488
+ saveDirty() {
489
+ clearTimeout(this.flush);
490
+ this.flush = null;
491
+ for (const p of [...this.dirty]) this.save(this.fs, p);
492
+ }
493
+
432
494
  // A file or a directory, and all under it.
433
495
  remove(path) {
434
496
  for (const t of ['beam_fs', 'beam_fs_chunk']) {
@@ -448,6 +510,7 @@ class Persist {
448
510
  // Wrap the functions of FS that change files (the system calls of the
449
511
  // VM call them).
450
512
  attach(FS) {
513
+ this.fs = FS;
451
514
  const abs = (p) => (typeof p === 'string' && !p.startsWith('/') ? `${FS.cwd()}/${p}` : p);
452
515
  const wrap = (name, after) => {
453
516
  const f = FS[name];
@@ -460,10 +523,7 @@ class Persist {
460
523
  wrap('write', (stream) => {
461
524
  if (!this.under(stream.path)) return;
462
525
  this.dirty.add(stream.path);
463
- this.flush ??= setTimeout(() => {
464
- this.flush = null;
465
- for (const p of [...this.dirty]) this.save(FS, p);
466
- }, 1000);
526
+ this.flush ??= setTimeout(() => this.saveDirty(), 1000);
467
527
  });
468
528
  wrap('close', (stream) => {
469
529
  if (this.under(stream.path) && ((stream.flags & 3) !== 0 || this.dirty.has(stream.path))) this.save(FS, stream.path);
@@ -538,6 +598,8 @@ export default {
538
598
  if (!vm) {
539
599
  const v = vm = new Vm(env, { host: new URL(request.url).hostname });
540
600
  v.ready.catch(() => { if (vm === v) vm = undefined; });
601
+ // A VM that stopped: the next request starts a new one.
602
+ v.onDead = () => { if (vm === v) vm = undefined; };
541
603
  }
542
604
  return vm.fetch(request, ctx);
543
605
  },
@@ -1085,6 +1147,22 @@ export class Vm {
1085
1147
  this.nextTimer = 1;
1086
1148
  this.env = env;
1087
1149
  this.waitListen = new Map(); // port -> the resolve functions of listening()
1150
+ // BEAM_YIELD_REDS (1000000, about 50 ms of work; "0": none): the
1151
+ // reductions between the timers of the turns (see turn).
1152
+ this.turns = 0;
1153
+ this.turnEvery = Math.ceil(limit(env.BEAM_YIELD_REDS, 1000000) / 20000);
1154
+ this.conns = new Set(); // the open connections of bridge() (see die)
1155
+ this.sockets = 0; // the open WebSockets: bridge() and /.tcp/PORT
1156
+ this.peak = 0; // the memory (MB) of the last log of the peak
1157
+ // The VM stopped (erlang:halt, or a trap such as an allocation that
1158
+ // failed): dead is the reason, and died resolves. onDead: the owner of
1159
+ // the VM drops it, so that a new VM takes the next request.
1160
+ this.dead = null;
1161
+ this.died = new Promise((resolve) => { this.markDead = resolve; });
1162
+ // The waits that the stop of the VM ends (stopWait): a race with
1163
+ // this.died itself keeps one reaction for each race until the VM stops.
1164
+ this.stopWaits = new Set();
1165
+ this.onDead = null;
1088
1166
  // The static files of the release (appFiles of app-com.js), when the
1089
1167
  // release is loaded: a request for one of them needs no VM.
1090
1168
  this.statics = new Promise((resolve) => { this.gotStatics = resolve; });
@@ -1139,6 +1217,13 @@ export class Vm {
1139
1217
  console.log('beam: the snapshot of the build is for another release: a boot in its place');
1140
1218
  built = null;
1141
1219
  }
1220
+ // A snapshot of the build with other flags of the emulator: a boot in
1221
+ // its place (npx beam.com --snapshot --env BEAM_ERL_FLAGS=...). A
1222
+ // snapshot with no flags in its header was made with none.
1223
+ if (built && (built.flags ?? '') !== erlFlags(env)) {
1224
+ console.log(`beam: the snapshot of the build has the flags "${built.flags ?? ''}", and BEAM_ERL_FLAGS is "${erlFlags(env)}": a boot in its place`);
1225
+ built = null;
1226
+ }
1142
1227
  // A Durable Object with Ecto SQLite (sql of .release.json): the
1143
1228
  // snapshot is made at the boot point (wasm_host_server), before the
1144
1229
  // program starts and runs its migrations. So all the objects (the
@@ -1159,6 +1244,7 @@ export class Vm {
1159
1244
  }
1160
1245
  const fromFallback = !snap && !!fallback;
1161
1246
  if (fromFallback) snap = fallback;
1247
+ if (snap) await inflateSnapshot(snap);
1162
1248
  let snapBytes = snap ? snap.pagesData : null;
1163
1249
  const nifs = await nifModules();
1164
1250
  // restored: this VM comes from a snapshot (the page shows it).
@@ -1171,7 +1257,7 @@ export class Vm {
1171
1257
  const t1 = Date.now();
1172
1258
  return new Promise((resolve, reject) => {
1173
1259
  const snapKB = snapBytes ? snapBytes.byteLength >> 10 : 0;
1174
- this.onready = () => { console.log(`beam: ready in ${Date.now() - t0} ms (release ${release.byteLength >> 10} KB${snap ? `, snapshot ${snapKB} KB` : ''} in ${t1 - t0} ms), ${this.memory()}`); resolve(); };
1260
+ this.onready = () => { this.peak = this.megabytes(); console.log(`beam: ready in ${Date.now() - t0} ms (release ${release.byteLength >> 10} KB${snap ? `, snapshot ${snapKB} KB` : ''} in ${t1 - t0} ms), ${this.memory()}`); resolve(); };
1175
1261
  createBeam({
1176
1262
  noInitialRun: !!snap,
1177
1263
  onRuntimeInitialized: snap ? () => {
@@ -1199,6 +1285,7 @@ export class Vm {
1199
1285
  } : undefined,
1200
1286
  // The boot arguments are set in preRun, after the release is unpacked.
1201
1287
  arguments: [],
1288
+ jspiTurn: (f) => this.turn(f),
1202
1289
  // A plain Worker runs the jobs and timers of the threads in its
1203
1290
  // open requests. After adopt() (a Durable Object), they run on a
1204
1291
  // MessageChannel and on setTimeout.
@@ -1278,15 +1365,20 @@ export class Vm {
1278
1365
  m.beamHost.onsend = (bytes) => this.onsend(bytes);
1279
1366
  if (this.hostFiles) m.beamHost.files = this.hostFiles;
1280
1367
  }],
1281
- print: (s) => console.log(s),
1282
- printErr: (s) => console.log(s),
1368
+ print: (s) => this.log(s),
1369
+ printErr: (s) => this.log(s),
1283
1370
  nifModule: (file) => nifModule(nifs, file),
1284
1371
  // Workers compile no WebAssembly at run time: use the imported module.
1285
1372
  instantiateWasm: (imports, done) => {
1286
1373
  WebAssembly.instantiate(wasm, imports).then((instance) => { this.exports = instance.exports; done(instance); });
1287
1374
  return {};
1288
1375
  },
1289
- onExit: (code) => reject(new Error(`beam exited with status ${code}`)),
1376
+ // In a plain Worker, the thread that stopped can run in the context
1377
+ // of a request that ended: die runs in an open request (run).
1378
+ onExit: (code) => {
1379
+ reject(new Error(`beam exited with status ${code}`));
1380
+ this.run(() => this.die(`exit status ${code}`));
1381
+ },
1290
1382
  }).catch(reject);
1291
1383
  });
1292
1384
  }
@@ -1368,7 +1460,7 @@ export class Vm {
1368
1460
  for (let i = 0; i < 5; i++) await tick();
1369
1461
  }
1370
1462
  try {
1371
- return capture(this.beam, this.release, this.listeners, bootPoint, (p) => !!this.persist?.under(p));
1463
+ return capture(this.beam, this.release, this.listeners, bootPoint, (p) => !!this.persist?.under(p), erlFlags(this.env));
1372
1464
  } finally {
1373
1465
  x.erts_wasm_resume();
1374
1466
  }
@@ -1446,11 +1538,119 @@ export class Vm {
1446
1538
  }
1447
1539
  }
1448
1540
 
1541
+ // A turn for a scheduler that computes (jspi_host_turn of jspi_lib.js,
1542
+ // each 20000 reductions with no wait), so that the I/O of the host (new
1543
+ // requests, sockets) runs while a process computes:
1544
+ // - Deno: setImmediate, which runs after the poll of the I/O, and costs
1545
+ // little;
1546
+ // - a Durable Object (and a web page): one turn for each BEAM_YIELD_REDS
1547
+ // reductions waits for a timer (about 1 ms or more); the other turns
1548
+ // are tasks;
1549
+ // - a plain Worker: only tasks. A timer there resumes the thread in the
1550
+ // request that fired it, and that request can end before the work.
1551
+ turn(f) {
1552
+ if (this.plain) {
1553
+ this.jobs.push(f);
1554
+ return void this.handlers.at(-1)?.wake?.();
1555
+ }
1556
+ if (typeof Deno == 'object') return void setImmediate(f);
1557
+ if (this.turnEvery && ++this.turns % this.turnEvery === 0) return void setTimeout(f, 0);
1558
+ this.post(f);
1559
+ }
1560
+
1561
+ // The linear memory of the VM in MB. It never shrinks, so it is also its
1562
+ // peak.
1563
+ megabytes() {
1564
+ return this.beam ? this.beam.HEAPU8.length >> 20 : 0;
1565
+ }
1566
+
1449
1567
  memory() {
1450
- return `memory ${this.beam.HEAPU8.length >> 20} MB`;
1568
+ return `memory ${this.megabytes()} MB`;
1569
+ }
1570
+
1571
+ // A log line each time the memory of the VM grew by 8 MB or more since
1572
+ // the last one (a Worker has 128 MB for all of the isolate).
1573
+ notePeak() {
1574
+ const mb = this.megabytes();
1575
+ if (mb < this.peak + 8) return;
1576
+ this.peak = mb;
1577
+ this.log(`beam: memory ${mb} MB (a new peak of this VM)`);
1578
+ }
1579
+
1580
+ // The VM stopped: each open request gets 503, each WebSocket closes with
1581
+ // 1011, and the owner drops the VM (onDead). The threads of the VM do not
1582
+ // run again, so without this the open requests and all later ones wait
1583
+ // for an answer that does not come.
1584
+ die(reason) {
1585
+ if (this.dead) return;
1586
+ this.dead = reason;
1587
+ try { this.persist?.saveDirty(); } catch (e) { this.log(`beam: persist: the save at the stop failed (${e.message})`); }
1588
+ const open = [...this.conns].filter((c) => !c.status).length;
1589
+ this.log(`beam: the VM stopped (${reason}), ${this.memory()}: ${open} open requests get 503`);
1590
+ // Each connection in its own request (run): in a plain Worker, its
1591
+ // response, its socket and its timer belong to that request.
1592
+ for (const c of this.conns) {
1593
+ this.bridgeRoom(c);
1594
+ const status = c.status;
1595
+ if (!status) c.status = 503;
1596
+ if (c.ws) this.socketGone(c);
1597
+ this.run(() => {
1598
+ clearTimeout(c.timer);
1599
+ if (!status) {
1600
+ c.resolve(this.stopped());
1601
+ c.finished();
1602
+ } else if (c.ws) {
1603
+ try { c.ws.close(1011, 'the app stopped'); } catch {}
1604
+ } else if (!c.done) {
1605
+ c.done = true;
1606
+ c.writer?.abort(new Error('the app stopped')).catch(() => {});
1607
+ }
1608
+ this.bridgeRelease(c);
1609
+ }, c.h);
1610
+ }
1611
+ this.conns.clear();
1612
+ // The other sockets: /.tcp/PORT, and the connections of the VM.
1613
+ for (const [id, t] of this.tcps) {
1614
+ if (!id.startsWith('b')) this.run(() => { try { t.close(); } catch {} }, t.h);
1615
+ }
1616
+ this.tcps.clear();
1617
+ // The fetch() calls of the VM: no one reads their bodies now.
1618
+ for (const ac of this.fetches?.values() ?? []) ac.abort();
1619
+ this.markDead();
1620
+ for (const resolve of this.stopWaits ?? []) resolve();
1621
+ this.stopWaits?.clear();
1622
+ this.onDead?.(reason);
1623
+ }
1624
+
1625
+ // A log line. In a plain Worker, a thread of the VM can run in the
1626
+ // context of a request that ended, where console.log throws: then an open
1627
+ // request writes the line (run).
1628
+ log(text) {
1629
+ try { console.log(text); } catch { this.run(() => console.log(text)); }
1630
+ }
1631
+
1632
+ // The answer of a VM that stopped. The owner starts a new VM for the
1633
+ // next request.
1634
+ stopped() {
1635
+ return new Response('The app stopped. Try again.\n',
1636
+ { status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
1637
+ }
1638
+
1639
+ // The answer above a limit of the host (BEAM_MAX_REQUESTS,
1640
+ // BEAM_MAX_WEBSOCKETS): before the VM gets the request or its body.
1641
+ busy(what) {
1642
+ console.log(`beam: too many ${what}: 503`);
1643
+ return new Response(`The app has too many ${what}. Try again.\n`,
1644
+ { status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
1645
+ }
1646
+
1647
+ // A WebSocket of the VM closed (once for each one).
1648
+ socketGone(c) {
1649
+ if (c.counted) { c.counted = false; this.sockets--; }
1451
1650
  }
1452
1651
 
1453
1652
  event(header, body) {
1653
+ if (this.dead) return;
1454
1654
  const h = new TextEncoder().encode(JSON.stringify(header) + '\n');
1455
1655
  if (!body || !body.byteLength) return this.beam.beamHost.push(h);
1456
1656
  const b = new Uint8Array(h.length + body.byteLength);
@@ -1479,6 +1679,10 @@ export class Vm {
1479
1679
  finished();
1480
1680
  return file;
1481
1681
  }
1682
+ if (this.dead) {
1683
+ finished();
1684
+ return this.stopped();
1685
+ }
1482
1686
  await this.ready;
1483
1687
  if (this.resume) {
1484
1688
  const f = this.resume;
@@ -1488,7 +1692,13 @@ export class Vm {
1488
1692
  // The first request makes the snapshot, and the other requests wait for
1489
1693
  // it, so that the snapshot has the state of no request.
1490
1694
  if (this.makeKey) this.snapping = this.makeSnapshot().finally(() => { this.snapping = null; });
1491
- if (this.snapping) await this.snapping;
1695
+ // A snapshot that takes too long does not hold the request: the VM
1696
+ // goes on, and the snapshot ends later or fails.
1697
+ if (this.snapping) await this.stopWait(this.snapping, SNAPSHOT_WAIT);
1698
+ if (this.dead) {
1699
+ finished();
1700
+ return this.stopped();
1701
+ }
1492
1702
  const url = new URL(request.url);
1493
1703
  const upgrade = request.headers.get('upgrade')?.toLowerCase() === 'websocket';
1494
1704
  const tcp = upgrade && url.pathname.match(/^\/\.tcp\/(\d+)$/);
@@ -1608,6 +1818,37 @@ export class Vm {
1608
1818
  return new Promise((r) => this.waitListen.set(port, [...(this.waitListen.get(port) ?? []), r]));
1609
1819
  }
1610
1820
 
1821
+ // The first of promise, the stop of the VM, and ms milliseconds (LATE).
1822
+ // The wait leaves nothing behind when it ends.
1823
+ async stopWait(promise, ms = 0) {
1824
+ if (this.dead) return;
1825
+ let resolve, timer;
1826
+ const stop = new Promise((r) => { resolve = r; });
1827
+ (this.stopWaits ??= new Set()).add(resolve);
1828
+ if (ms) timer = setTimeout(() => resolve(LATE), ms);
1829
+ try {
1830
+ return await Promise.race([promise, stop]);
1831
+ } finally {
1832
+ clearTimeout(timer);
1833
+ this.stopWaits.delete(resolve);
1834
+ }
1835
+ }
1836
+
1837
+ // The wait of a request for the listener of port, or null when it
1838
+ // listens. cancel() removes the wait of a request that ends first.
1839
+ listenWait(port) {
1840
+ if (this.listeners.has(port)) return null;
1841
+ let resolve;
1842
+ const promise = new Promise((r) => { resolve = r; });
1843
+ this.waitListen.set(port, [...(this.waitListen.get(port) ?? []), resolve]);
1844
+ const cancel = () => {
1845
+ const rest = (this.waitListen.get(port) ?? []).filter((r) => r !== resolve);
1846
+ if (rest.length) this.waitListen.set(port, rest);
1847
+ else this.waitListen.delete(port);
1848
+ };
1849
+ return { promise, cancel };
1850
+ }
1851
+
1611
1852
  // The Origin that the app gets. Phoenix compares the Origin of a
1612
1853
  // WebSocket with the host of its config (PHX_HOST), so a page of the app on
1613
1854
  // another name of the Worker (wrangler dev, a custom domain, a preview URL,
@@ -1623,9 +1864,53 @@ export class Vm {
1623
1864
 
1624
1865
  // The request as a TCP connection to the HTTP server of the
1625
1866
  // app on PORT. Bandit does the HTTP; here only the bytes are framed.
1867
+ // The limits of the host (vars, 0 for no limit):
1868
+ // - BEAM_MAX_REQUESTS: the requests that wait for the app at one time
1869
+ // (no limit by default); one more gets 503 before the VM reads it;
1870
+ // - BEAM_MAX_WEBSOCKETS: the open WebSockets (no limit by default);
1871
+ // - BEAM_REQUEST_TIMEOUT: the seconds until the head of the response
1872
+ // (60 by default), after the end of the request body; then the request
1873
+ // gets 504, and the app gets the end of the connection;
1874
+ // - BEAM_MAX_BODY: the bytes of a request body (no limit by default);
1875
+ // a larger one gets 413.
1876
+ // The body goes to the app in the chunks of the client (bridgeUpload).
1626
1877
  async bridge(request, url, upgrade, h, finished) {
1878
+ const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
1879
+ if (upgrade && maxSockets && this.sockets >= maxSockets) {
1880
+ finished();
1881
+ return this.busy('WebSockets');
1882
+ }
1883
+ const maxRequests = limit(this.env.BEAM_MAX_REQUESTS, 0);
1884
+ if (!upgrade && maxRequests && [...this.conns].filter((c) => !c.ws).length >= maxRequests) {
1885
+ finished();
1886
+ return this.busy('requests');
1887
+ }
1888
+ const body = request.method === 'GET' || request.method === 'HEAD' || upgrade ? null : request.body;
1889
+ const declared = request.headers.get('content-length');
1890
+ const maxBody = limit(this.env.BEAM_MAX_BODY, 0);
1891
+ if (body && maxBody && declared !== null && Number(declared) > maxBody) {
1892
+ finished();
1893
+ return this.tooLarge(maxBody);
1894
+ }
1627
1895
  const port = Number(this.env.PORT ?? 4000);
1628
- await this.listening(port);
1896
+ // The time limit counts from here: the app can also fail to listen.
1897
+ const timeout = limit(this.env.BEAM_REQUEST_TIMEOUT, 60);
1898
+ const deadline = Date.now() + timeout * 1000;
1899
+ // Only a request that comes before the app listens waits here.
1900
+ const listen = this.listenWait(port);
1901
+ let waited;
1902
+ if (listen) {
1903
+ waited = await this.stopWait(listen.promise, timeout * 1000);
1904
+ listen.cancel();
1905
+ }
1906
+ if (this.dead) {
1907
+ finished();
1908
+ return this.stopped();
1909
+ }
1910
+ if (waited === LATE) {
1911
+ finished();
1912
+ return this.late(`the app did not listen on port ${port}`, timeout);
1913
+ }
1629
1914
  const id = `b${this.nextId++}`;
1630
1915
  const headers = new Headers(request.headers);
1631
1916
  headers.set('host', url.host);
@@ -1638,6 +1923,8 @@ export class Vm {
1638
1923
  if (origin) headers.set('origin', origin);
1639
1924
  headers.delete('transfer-encoding');
1640
1925
  headers.delete('sec-websocket-extensions'); // no compression: frames as they are
1926
+ // The host has the whole request: no "100 Continue" from the app.
1927
+ headers.delete('expect');
1641
1928
  // The Workers runtime compresses the response for each client. The app
1642
1929
  // gets only gzip of Accept-Encoding: Plug.Static serves the files that
1643
1930
  // are gzipped already (Livebook has no other ones), and the response
@@ -1652,43 +1939,184 @@ export class Vm {
1652
1939
  } else {
1653
1940
  headers.set('connection', 'close');
1654
1941
  }
1655
- const body = request.method === 'GET' || request.method === 'HEAD' || upgrade
1656
- ? new Uint8Array(0) : new Uint8Array(await request.arrayBuffer());
1657
- if (body.length || !['GET', 'HEAD'].includes(request.method)) headers.set('content-length', String(body.length));
1942
+ // A body of no declared length goes to the app as chunked.
1943
+ if (body && declared !== null) headers.set('content-length', declared);
1944
+ else if (body) {
1945
+ headers.delete('content-length');
1946
+ headers.set('transfer-encoding', 'chunked');
1947
+ } else if (!upgrade && !['GET', 'HEAD'].includes(request.method)) headers.set('content-length', '0');
1658
1948
  let head = `${request.method} ${url.pathname}${url.search} HTTP/1.1\r\n`;
1659
1949
  for (const [k, v] of headers) head += `${k}: ${v}\r\n`;
1660
- head = new TextEncoder().encode(head + '\r\n');
1661
- const bytes = new Uint8Array(head.length + body.length);
1662
- bytes.set(head);
1663
- bytes.set(body, head.length);
1950
+ // The values of Headers are bytes (one character for each byte).
1951
+ const bytes = latin1Bytes(head + '\r\n');
1952
+ if (this.dead) {
1953
+ finished();
1954
+ return this.stopped();
1955
+ }
1664
1956
  return new Promise((resolve) => {
1665
- const c = { id, buf: new Uint8Array(0), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname };
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 };
1666
1958
  if (h) h.sockets++;
1667
- this.tcps.set(id, { send: (b) => this.bridgeData(c, b), close: () => this.bridgeEnd(c), h });
1668
- 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 });
1669
- this.event({ t: 'tcp_data', id }, bytes);
1959
+ this.conns.add(c);
1960
+ c.timeout = timeout;
1961
+ // A body can take long to come: the time limit starts at its end.
1962
+ if (!body) this.bridgeClock(c, deadline - Date.now());
1963
+ // An error of one connection ends that connection, not the VM.
1964
+ // A send ends when the client read its bytes (the newest write of
1965
+ // the response, c.wrote): tcpSend then tells the VM (tcp_sent).
1966
+ this.tcps.set(id, {
1967
+ send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
+ close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
+ ack: (n) => this.bridgeRead(c, n), h,
1970
+ });
1971
+ // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
+ // body of the request, and the messages of a WebSocket. sent: the
1973
+ // host tells wasm_tcp what the client took (tcp_sent).
1974
+ 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);
1670
1977
  });
1671
1978
  }
1672
1979
 
1980
+ // Bytes to the app on the connection c.
1981
+ bridgeSend(c, bytes) {
1982
+ c.sent += bytes.length;
1983
+ this.event({ t: 'tcp_data', id: c.id }, bytes);
1984
+ }
1985
+
1986
+ // The body of a request to the app, in parts of UPLOAD_PART bytes or more
1987
+ // (or the rest of the body): one event for each part, not for each small
1988
+ // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1989
+ // 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) {
1992
+ // workerd has readAtLeast. The option min of the standard BYOB read is
1993
+ // not safe: when the stream closes with fewer bytes than min, the read
1994
+ // errors the stream (Node.js 26, Chromium). So the other hosts use the
1995
+ // default reader, and the parts come from the chunks here.
1996
+ let byob = null;
1997
+ try { byob = body.getReader({ mode: 'byob' }); } catch {}
1998
+ if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
1999
+ const reader = byob ?? body.getReader();
2000
+ const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2001
+ let total = 0;
2002
+ let part = [];
2003
+ let size = 0;
2004
+ // The gathered part to the app, when less than UPLOAD_WINDOW bytes are
2005
+ // unread. False when the connection ended while it waited.
2006
+ const flush = async () => {
2007
+ while (c.sent - c.read >= UPLOAD_WINDOW && !c.done && !this.dead) {
2008
+ await new Promise((resolve) => { c.room = resolve; });
2009
+ }
2010
+ if (c.done || this.dead) return false;
2011
+ const bytes = part.length === 1 ? part[0] : join(part, size);
2012
+ part = [];
2013
+ size = 0;
2014
+ this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
2015
+ return true;
2016
+ };
2017
+ try {
2018
+ for (;;) {
2019
+ // A read can end the stream with its last bytes (done and a value).
2020
+ const { value, done } = await read();
2021
+ if (c.done || this.dead) break;
2022
+ if (value?.length) {
2023
+ total += value.length;
2024
+ if (max && total > max) {
2025
+ this.bridgeRefuse(c, this.tooLarge(max));
2026
+ break;
2027
+ }
2028
+ }
2029
+ // A chunk of the client can be large (Deno): it goes in pieces of
2030
+ // UPLOAD_READ bytes or less, so the window stays small.
2031
+ let ok = true;
2032
+ for (let at = 0; ok && value && at < value.length; at += UPLOAD_READ) {
2033
+ const piece = value.subarray(at, at + UPLOAD_READ);
2034
+ part.push(piece);
2035
+ size += piece.length;
2036
+ if (size >= UPLOAD_PART) ok = await flush();
2037
+ }
2038
+ if (ok && done && size) ok = await flush();
2039
+ if (!ok) break;
2040
+ if (done) {
2041
+ if (chunked) this.bridgeSend(c, CHUNKS_END);
2042
+ this.bridgeClock(c, c.timeout * 1000);
2043
+ return;
2044
+ }
2045
+ }
2046
+ reader.cancel().catch(() => {});
2047
+ } catch {
2048
+ // The client stopped the upload.
2049
+ this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2050
+ }
2051
+ }
2052
+
2053
+ // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
2054
+ bridgeClock(c, ms) {
2055
+ if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2056
+ }
2057
+
2058
+ // The app read n bytes of the connection c (tcp_read).
2059
+ bridgeRead(c, n) {
2060
+ c.read += n;
2061
+ this.bridgeRoom(c);
2062
+ c.inbound?.flush();
2063
+ }
2064
+
2065
+ // The upload of c waits no more (more room, or the end of c).
2066
+ bridgeRoom(c) {
2067
+ const room = c.room;
2068
+ c.room = null;
2069
+ room?.();
2070
+ }
2071
+
2072
+ // The answer of the host in place of the app (when the app has not
2073
+ // answered yet), and the end of the connection for the app.
2074
+ bridgeRefuse(c, response) {
2075
+ if (!c.status) {
2076
+ clearTimeout(c.timer);
2077
+ c.status = response.status;
2078
+ c.resolve(response);
2079
+ c.finished();
2080
+ }
2081
+ this.bridgeDone(c);
2082
+ }
2083
+
2084
+ // The answer above BEAM_MAX_BODY.
2085
+ tooLarge(max) {
2086
+ this.log(`beam: a request body above ${max} bytes: 413`);
2087
+ return new Response(`The body is larger than ${max} bytes.\n`, { status: 413, headers: { 'content-type': 'text/plain' } });
2088
+ }
2089
+
1673
2090
  // Bytes from the HTTP server of the app to the connection c of bridge().
1674
2091
  bridgeData(c, data) {
1675
- const b = new Uint8Array(c.buf.length + data.length);
1676
- b.set(c.buf);
1677
- b.set(data, c.buf.length);
1678
- c.buf = b;
1679
- if (!c.status) {
1680
- const end = indexOf(c.buf, [13, 10, 13, 10]);
1681
- if (end < 0) return;
1682
- const lines = new TextDecoder().decode(c.buf.subarray(0, end)).split('\r\n');
1683
- c.status = Number(lines[0].split(' ')[1]);
2092
+ c.in.push(data);
2093
+ while (!c.status) {
2094
+ // The head of the response: it is small, so the bytes are joined.
2095
+ const head = c.in.peek(c.in.size);
2096
+ const end = indexOf(head, [13, 10, 13, 10]);
2097
+ if (end < 0) {
2098
+ if (head.length > HEAD_MAX) throw new Error(`a head of a response above ${HEAD_MAX} bytes`);
2099
+ return;
2100
+ }
2101
+ // The bytes of a header value are characters of one byte (Headers
2102
+ // takes no character above U+00FF).
2103
+ const lines = latin1(head.subarray(0, end)).split('\r\n');
2104
+ c.in.drop(end + 4);
2105
+ const status = Number(lines[0].split(' ')[1]);
2106
+ // An informational head (100 Continue, 103 Early Hints): the head of
2107
+ // the response comes after it.
2108
+ if (status >= 100 && status < 200 && !(status === 101 && c.upgrade)) continue;
1684
2109
  const headers = new Headers();
1685
2110
  for (const l of lines.slice(1)) {
1686
2111
  const i = l.indexOf(':');
1687
2112
  headers.append(l.slice(0, i).trim(), l.slice(i + 1).trim());
1688
2113
  }
1689
- c.buf = c.buf.slice(end + 4);
1690
- if (c.status === 101 && c.upgrade) return this.bridgeUpgrade(c);
1691
- if (c.upgrade && c.status === 403) {
2114
+ clearTimeout(c.timer);
2115
+ if (status === 101) {
2116
+ c.status = 101;
2117
+ return this.bridgeUpgrade(c);
2118
+ }
2119
+ if (c.upgrade && status === 403) {
1692
2120
  console.log(`beam: the app refused the WebSocket of ${c.path} (403) from the origin ${c.origin}. ` +
1693
2121
  `A Phoenix app compares the Origin with the host of its config (PHX_HOST=${this.env.PHX_HOST ?? this.vars.PHX_HOST ?? ''}): see check_origin.`);
1694
2122
  }
@@ -1696,13 +2124,21 @@ export class Vm {
1696
2124
  c.chunked = /chunked/i.test(headers.get('transfer-encoding') ?? '');
1697
2125
  headers.delete('transfer-encoding');
1698
2126
  headers.delete('connection');
1699
- if (c.head || c.status === 204 || c.status === 304) c.length = 0;
2127
+ if (c.head || status === 204 || status === 304) { c.length = 0; c.chunked = false; }
2128
+ c.left = 0;
1700
2129
  const { readable, writable } = new TransformStream();
1701
- c.writer = writable.getWriter();
1702
2130
  // The app compressed the body (Bandit: gzip, deflate): the runtime
1703
2131
  // must send it as it is, not compress it again.
1704
2132
  const encodeBody = headers.has('content-encoding') ? 'manual' : 'automatic';
1705
- c.resolve(new Response(c.length === 0 ? null : readable, { status: c.status, headers, encodeBody }));
2133
+ // The Response first: a status that it refuses ends the connection
2134
+ // with 502 (bridgeGuard), and the client gets an answer.
2135
+ const response = new Response(c.length === 0 ? null : readable, { status, headers, encodeBody });
2136
+ c.status = status;
2137
+ c.writer = writable.getWriter();
2138
+ // The client went away (or the stream failed): the app gets the end
2139
+ // of the connection.
2140
+ c.writer.closed.catch(() => this.bridgeDone(c));
2141
+ c.resolve(response);
1706
2142
  c.finished();
1707
2143
  }
1708
2144
  if (c.ws) return this.bridgeFrames(c);
@@ -1710,32 +2146,95 @@ export class Vm {
1710
2146
  }
1711
2147
 
1712
2148
  // The body of a response: by content-length, chunked, or until the end.
2149
+ // The pieces of the app go to the stream as they are (views, no copy):
2150
+ // also the data of a large chunk, before its end.
1713
2151
  bridgeBody(c) {
1714
2152
  if (c.done) return;
2153
+ const pieces = c.in;
1715
2154
  if (c.chunked) {
1716
- for (;;) {
1717
- const nl = indexOf(c.buf, [13, 10]);
1718
- if (nl < 0) return;
1719
- const size = parseInt(new TextDecoder().decode(c.buf.subarray(0, nl)), 16);
2155
+ while (pieces.size && !c.done) {
2156
+ if (c.left > 0) {
2157
+ const part = pieces.next(c.left);
2158
+ c.left -= part.length;
2159
+ this.bridgeWrite(c, part);
2160
+ if (c.left === 0) c.left = -2; // then the CRLF of the chunk
2161
+ continue;
2162
+ }
2163
+ if (c.left < 0) {
2164
+ const n = Math.min(-c.left, pieces.size);
2165
+ pieces.drop(n);
2166
+ c.left += n;
2167
+ continue;
2168
+ }
2169
+ // The size line of the next chunk (a chunk extension after ";").
2170
+ const line = pieces.peek(Math.min(pieces.size, CHUNK_LINE));
2171
+ const nl = line.indexOf(10);
2172
+ if (nl < 0) {
2173
+ if (line.length >= CHUNK_LINE) this.bridgeBad(c);
2174
+ return;
2175
+ }
2176
+ const size = parseInt(new TextDecoder().decode(line.subarray(0, nl)), 16);
2177
+ pieces.drop(nl + 1);
1720
2178
  if (size === 0) return this.bridgeDone(c);
1721
- if (c.buf.length < nl + 2 + size + 2) return;
1722
- c.writer.write(c.buf.slice(nl + 2, nl + 2 + size));
1723
- c.buf = c.buf.slice(nl + 2 + size + 2);
2179
+ if (!(size > 0)) return this.bridgeBad(c);
2180
+ c.left = size;
1724
2181
  }
2182
+ return;
1725
2183
  }
1726
- if (c.buf.length) {
1727
- const part = c.length === null ? c.buf : c.buf.subarray(0, c.length);
1728
- if (part.length) c.writer.write(part.slice());
2184
+ while (pieces.size && c.length !== 0) {
2185
+ const part = pieces.next(c.length ?? Infinity);
1729
2186
  if (c.length !== null) c.length -= part.length;
1730
- c.buf = new Uint8Array(0);
2187
+ this.bridgeWrite(c, part);
1731
2188
  }
1732
2189
  if (c.length === 0) this.bridgeDone(c);
1733
2190
  }
1734
2191
 
2192
+ // A part of the body to the client. A write to a stream that the client
2193
+ // closed fails, and writer.closed then ends the connection.
2194
+ bridgeWrite(c, part) {
2195
+ if (c.writer) c.wrote = c.writer.write(part).catch(() => {});
2196
+ }
2197
+
2198
+ // fn for the connection c (an event of the app). An exception ends c:
2199
+ // 502 before the head of the response, else the end of the stream or of
2200
+ // the WebSocket. Before, it stopped the thread of the VM, and so the VM.
2201
+ bridgeGuard(c, fn) {
2202
+ try {
2203
+ fn();
2204
+ } catch (e) {
2205
+ this.log(`beam: the response of ${c.path} failed (${e.message}): the connection ends`);
2206
+ if (c.ws) {
2207
+ try { c.ws.close(1011); } catch {}
2208
+ this.socketGone(c);
2209
+ this.conns.delete(c);
2210
+ this.tcps.delete(c.id);
2211
+ this.event({ t: 'tcp_closed', id: c.id });
2212
+ this.bridgeRelease(c);
2213
+ } else if (!c.status) {
2214
+ this.bridgeRefuse(c, new Response('bad gateway\n', { status: 502 }));
2215
+ } else {
2216
+ c.writer?.abort(e).catch(() => {});
2217
+ c.writer = null;
2218
+ this.bridgeDone(c);
2219
+ }
2220
+ }
2221
+ }
2222
+
2223
+ // A chunked body that is not HTTP/1.1: the stream of the response
2224
+ // fails, and the app gets the end of the connection.
2225
+ bridgeBad(c) {
2226
+ console.log(`beam: a bad chunked body from the app for ${c.path}: the response stops`);
2227
+ c.writer?.abort(new Error('a bad chunked body from the app')).catch(() => {});
2228
+ c.writer = null;
2229
+ this.bridgeDone(c);
2230
+ }
2231
+
1735
2232
  bridgeDone(c) {
1736
2233
  if (c.done) return;
1737
2234
  c.done = true;
2235
+ this.bridgeRoom(c);
1738
2236
  c.writer?.close().catch(() => {});
2237
+ this.conns.delete(c);
1739
2238
  this.tcps.delete(c.id);
1740
2239
  this.event({ t: 'tcp_closed', id: c.id });
1741
2240
  this.bridgeRelease(c);
@@ -1746,17 +2245,40 @@ export class Vm {
1746
2245
  if (c.released) return;
1747
2246
  c.released = true;
1748
2247
  if (c.h) { c.h.sockets--; c.h.wake?.(); }
2248
+ this.notePeak();
1749
2249
  }
1750
2250
 
1751
2251
  // The app closed the connection.
1752
2252
  bridgeEnd(c) {
2253
+ clearTimeout(c.timer);
1753
2254
  this.tcps.delete(c.id);
1754
2255
  if (!c.status) {
2256
+ c.status = 502;
1755
2257
  c.resolve(new Response('bad gateway\n', { status: 502 }));
1756
2258
  c.finished();
1757
2259
  }
1758
- if (c.ws) { try { c.ws.close(); } catch {} this.bridgeRelease(c); }
1759
- else this.bridgeDone(c);
2260
+ if (c.ws) {
2261
+ try { c.ws.close(); } catch {}
2262
+ this.socketGone(c);
2263
+ this.conns.delete(c);
2264
+ this.bridgeRelease(c);
2265
+ } else this.bridgeDone(c);
2266
+ }
2267
+
2268
+ // No head of a response in BEAM_REQUEST_TIMEOUT seconds: 504 for the
2269
+ // client, and the end of the connection for the app (tcp_closed).
2270
+ bridgeTimeout(c, seconds) {
2271
+ if (c.status || c.done) return;
2272
+ c.status = 504;
2273
+ c.resolve(this.late(`no response for ${c.path}`, seconds));
2274
+ c.finished();
2275
+ this.bridgeDone(c);
2276
+ }
2277
+
2278
+ // The answer of BEAM_REQUEST_TIMEOUT.
2279
+ late(what, seconds) {
2280
+ console.log(`beam: ${what} in ${seconds} s: 504`);
2281
+ return new Response('The app did not answer in time.\n', { status: 504, headers: { 'content-type': 'text/plain' } });
1760
2282
  }
1761
2283
 
1762
2284
  // A WebSocket: the client end to the browser, frames to the app.
@@ -1765,14 +2287,23 @@ export class Vm {
1765
2287
  server.accept();
1766
2288
  server.binaryType = 'arraybuffer';
1767
2289
  c.ws = server;
2290
+ c.counted = true;
2291
+ this.sockets++;
1768
2292
  c.frames = []; // the parts of a message in fragments
2293
+ // The messages of the client go while less than UPLOAD_WINDOW bytes
2294
+ // are unread in the VM; the others wait here (Inbound).
2295
+ c.inbound = new Inbound(c, (b) => this.bridgeSend(c, b), () => {
2296
+ console.log(`beam: the app reads the WebSocket of ${c.path} too slowly: it closes (1008)`);
2297
+ closeSocket(server, 1008);
2298
+ });
1769
2299
  server.addEventListener('message', (e) => {
1770
2300
  const text = typeof e.data === 'string';
1771
- this.event({ t: 'tcp_data', id: c.id }, frame(text ? 1 : 2, text ? new TextEncoder().encode(e.data) : new Uint8Array(e.data)));
2301
+ c.inbound.push(frame(text ? 1 : 2, text ? new TextEncoder().encode(e.data) : new Uint8Array(e.data)));
1772
2302
  });
1773
2303
  server.addEventListener('close', (e) => {
1774
- const code = e.code && e.code !== 1005 ? e.code : 1000;
1775
- this.event({ t: 'tcp_data', id: c.id }, frame(8, new Uint8Array([code >> 8, code & 255])));
2304
+ this.socketGone(c);
2305
+ const code = wireCode(e.code);
2306
+ c.inbound.push(frame(8, new Uint8Array([code >> 8, code & 255])), true);
1776
2307
  });
1777
2308
  c.resolve(new Response(null, { status: 101, webSocket: client }));
1778
2309
  c.finished();
@@ -1781,18 +2312,21 @@ export class Vm {
1781
2312
 
1782
2313
  // WebSocket frames from the app (not masked) to messages for the client.
1783
2314
  bridgeFrames(c) {
2315
+ const pieces = c.in;
1784
2316
  for (;;) {
1785
- const b = c.buf;
2317
+ // The head of a frame (2 to 10 bytes), then its payload in one array.
2318
+ const b = pieces.peek(Math.min(pieces.size, 10));
1786
2319
  if (b.length < 2) return;
1787
2320
  let len = b[1] & 127, at = 2;
1788
2321
  if (len === 126) { if (b.length < 4) return; len = (b[2] << 8) | b[3]; at = 4; }
1789
2322
  else if (len === 127) { if (b.length < 10) return; len = Number(new DataView(b.buffer, b.byteOffset).getBigUint64(2)); at = 10; }
1790
- if (b.length < at + len) return;
1791
- const fin = b[0] & 128, op = b[0] & 15, data = b.slice(at, at + len);
1792
- c.buf = b.slice(at + len);
2323
+ if (pieces.size < at + len) return;
2324
+ const fin = b[0] & 128, op = b[0] & 15;
2325
+ pieces.drop(at);
2326
+ const data = pieces.take(len);
1793
2327
  if (op === 9) { this.event({ t: 'tcp_data', id: c.id }, frame(10, data)); continue; }
1794
2328
  if (op === 10) continue;
1795
- if (op === 8) { c.ws.close(len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
2329
+ if (op === 8) { closeSocket(c.ws, len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
1796
2330
  if (op !== 0) c.op = op;
1797
2331
  c.frames.push(data);
1798
2332
  if (!fin) continue;
@@ -1800,7 +2334,8 @@ export class Vm {
1800
2334
  let i = 0;
1801
2335
  for (const f of c.frames) { all.set(f, i); i += f.length; }
1802
2336
  c.frames = [];
1803
- c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all);
2337
+ // The client can close at any time: a send after it is dropped.
2338
+ try { c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all); } catch {}
1804
2339
  }
1805
2340
  }
1806
2341
 
@@ -1810,17 +2345,29 @@ export class Vm {
1810
2345
  const listener = this.listeners.get(port);
1811
2346
  finished();
1812
2347
  if (!listener) return new Response(`no listener on ${port}\n`, { status: 404 });
2348
+ const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
2349
+ if (maxSockets && this.sockets >= maxSockets) return this.busy('WebSockets');
1813
2350
  const id = `w${this.nextId++}`;
1814
2351
  const [client, server] = Object.values(new WebSocketPair());
1815
2352
  server.accept();
1816
2353
  server.binaryType = 'arraybuffer'; // (the default can be Blob)
1817
2354
  if (h) h.sockets++;
1818
- this.tcps.set(id, { send: (b) => server.send(b), close: () => server.close(), h });
1819
- this.event({ t: 'tcp_accept', id: listener, conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0 });
2355
+ const counted = { counted: true };
2356
+ this.sockets++;
2357
+ const t = { send: (b) => { try { server.send(b); } catch {} }, close: () => { this.socketGone(counted); try { server.close(); } catch {} }, h };
2358
+ const win = { sent: 0, read: 0 };
2359
+ const inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {
2360
+ console.log(`beam: the app reads the connection ${id} to port ${port} too slowly: it closes (1008)`);
2361
+ closeSocket(server, 1008);
2362
+ });
2363
+ t.ack = (n) => { win.read += n; inbound.flush(); };
2364
+ this.tcps.set(id, t);
2365
+ this.event({ t: 'tcp_accept', id: listener, conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: true, sent: true });
1820
2366
  server.addEventListener('message', (e) => {
1821
- this.tcpData(id, typeof e.data === 'string' ? new TextEncoder().encode(e.data) : e.data);
2367
+ inbound.push(typeof e.data === 'string' ? new TextEncoder().encode(e.data) : new Uint8Array(e.data));
1822
2368
  });
1823
2369
  server.addEventListener('close', () => {
2370
+ this.socketGone(counted);
1824
2371
  this.tcpClosed(id);
1825
2372
  if (h) { h.sockets--; h.wake?.(); }
1826
2373
  });
@@ -1833,7 +2380,7 @@ export class Vm {
1833
2380
  // first (fetchFirst); the other protocols use connect(). direct: the
1834
2381
  // tunnel of wasm_host_fetch, which never goes back to the fetch path.
1835
2382
  async tcpConnect({ id, host, port, direct }, h) {
1836
- let socket;
2383
+ let socket, inbound;
1837
2384
  if (h) h.sockets++;
1838
2385
  const allowed = connectAllowed(this.env.BEAM_CONNECT, host, port);
1839
2386
  const fetchPath = this.listeners.has('fetch') && !direct;
@@ -1843,11 +2390,21 @@ export class Vm {
1843
2390
  socket = net.connect({ host, port });
1844
2391
  // A send resolves when the bytes are written; a close sends the
1845
2392
  // bytes that wait, and then ends the socket.
1846
- this.tcps.set(id, {
2393
+ const t = {
1847
2394
  send: (b) => new Promise((resolve) => socket.write(b, () => resolve())),
1848
2395
  close: () => socket.end(() => socket.destroy()),
1849
2396
  h,
1850
- });
2397
+ };
2398
+ // The data of the peer goes while less than UPLOAD_WINDOW bytes are
2399
+ // unread in the VM; else the socket pauses.
2400
+ const win = { sent: 0, read: 0 };
2401
+ inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {});
2402
+ t.ack = (n) => {
2403
+ win.read += n;
2404
+ inbound.flush();
2405
+ if (!inbound.full()) socket.resume();
2406
+ };
2407
+ this.tcps.set(id, t);
1851
2408
  await new Promise((resolve, reject) => {
1852
2409
  socket.once('connect', resolve);
1853
2410
  socket.once('error', reject);
@@ -1869,9 +2426,12 @@ export class Vm {
1869
2426
  if (h) { h.sockets--; h.wake?.(); }
1870
2427
  return;
1871
2428
  }
1872
- this.event({ t: 'tcp_open', id });
2429
+ this.event({ t: 'tcp_open', id, ack: true, sent: true });
1873
2430
  await new Promise((resolve) => {
1874
- socket.on('data', (b) => this.tcpData(id, new Uint8Array(b.buffer, b.byteOffset, b.byteLength)));
2431
+ socket.on('data', (b) => {
2432
+ inbound.push(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), true);
2433
+ if (inbound.full()) socket.pause();
2434
+ });
1875
2435
  socket.on('error', () => {});
1876
2436
  socket.once('close', resolve);
1877
2437
  });
@@ -1908,6 +2468,9 @@ export class Vm {
1908
2468
  } catch (e) {
1909
2469
  console.log(`beam: resolve ${name}: ${e.message}`);
1910
2470
  }
2471
+ // At most DNS_MAX names: a new name removes the oldest.
2472
+ this.dns.delete(name);
2473
+ if (this.dns.size >= DNS_MAX) this.dns.delete(this.dns.keys().next().value);
1911
2474
  this.dns.set(name, { at: now, addresses });
1912
2475
  return addresses.some((a) => inRanges(a));
1913
2476
  }
@@ -1917,21 +2480,29 @@ export class Vm {
1917
2480
  // an open connection to host:port. The pair ends when one side closes.
1918
2481
  async fetchPair(id, host, port, h) {
1919
2482
  const conn = `x${this.nextId++}`;
1920
- let done;
2483
+ let done, pairEnd;
1921
2484
  const ended = new Promise((r) => { done = r; });
1922
2485
  const end = (other) => {
1923
2486
  if (!this.fetchConns.has(conn)) return;
1924
2487
  this.fetchConns.delete(conn);
1925
2488
  this.tcps.delete(id);
1926
2489
  this.tcps.delete(conn);
2490
+ // The fetches of this connection: no one reads their bodies now.
2491
+ for (const ac of this.fetches.values()) if (ac.conn === conn) ac.abort();
2492
+ pairEnd();
1927
2493
  this.event({ t: 'tcp_closed', id: other });
1928
2494
  done();
1929
2495
  };
1930
2496
  this.fetchConns.set(conn, { host, port, h });
1931
- this.tcps.set(id, { send: (b) => this.event({ t: 'tcp_data', id: conn }, b), close: () => end(conn), h });
1932
- this.tcps.set(conn, { send: (b) => this.event({ t: 'tcp_data', id }, b), close: () => end(id), h });
1933
- this.event({ t: 'tcp_open', id });
1934
- this.event({ t: 'tcp_accept', id: this.listeners.get('fetch'), conn, host, port });
2497
+ // A send of one socket ends when the other socket read the bytes
2498
+ // (tcp_read): so the window of the sender (tcp_sent) holds them.
2499
+ const toConn = relay((b) => this.event({ t: 'tcp_data', id: conn }, b));
2500
+ const toId = relay((b) => this.event({ t: 'tcp_data', id }, b));
2501
+ pairEnd = () => { toConn.end(); toId.end(); };
2502
+ this.tcps.set(id, { send: toConn.send, close: () => end(conn), ack: toId.read, h });
2503
+ this.tcps.set(conn, { send: toId.send, close: () => end(id), ack: toConn.read, h });
2504
+ this.event({ t: 'tcp_open', id, ack: true, sent: true });
2505
+ this.event({ t: 'tcp_accept', id: this.listeners.get('fetch'), conn, host, port, ack: true, sent: true });
1935
2506
  await ended;
1936
2507
  if (h) { h.sockets--; h.wake?.(); }
1937
2508
  }
@@ -1944,6 +2515,12 @@ export class Vm {
1944
2515
  if (h) h.sockets++;
1945
2516
  this.fetchPending++;
1946
2517
  const ac = new AbortController();
2518
+ ac.conn = msg.conn;
2519
+ // ack: wasm_host_fetch tells the host what it read (fetch_read). The
2520
+ // body then goes while less than UPLOAD_WINDOW bytes are unread.
2521
+ ac.sent = 0;
2522
+ ac.read = 0;
2523
+ ac.signal.addEventListener('abort', () => ac.room?.());
1947
2524
  this.fetches.set(msg.id, ac);
1948
2525
  try {
1949
2526
  const url = c && fetchUrl(c.host, c.port, msg.tls, msg.path);
@@ -1961,7 +2538,13 @@ export class Vm {
1961
2538
  for (;;) {
1962
2539
  const { done, value } = await reader.read();
1963
2540
  if (done) break;
1964
- if (value?.byteLength) this.event({ t: 'fetch_data', id: msg.id }, value);
2541
+ if (!value?.byteLength) continue;
2542
+ this.event({ t: 'fetch_data', id: msg.id }, value);
2543
+ ac.sent += value.byteLength;
2544
+ while (msg.ack && ac.sent - ac.read >= UPLOAD_WINDOW && !ac.signal.aborted) {
2545
+ await new Promise((resolve) => { ac.room = resolve; });
2546
+ }
2547
+ if (ac.signal.aborted) throw new Error('the fetch stopped');
1965
2548
  }
1966
2549
  }
1967
2550
  this.event({ t: 'fetch_end', id: msg.id });
@@ -1974,12 +2557,42 @@ export class Vm {
1974
2557
  }
1975
2558
  }
1976
2559
 
1977
- // Data of a TCP socket: to Erlang, or to its peer after a splice.
2560
+ // Data of a TCP socket: to Erlang (then true), or to its peer after a
2561
+ // splice.
1978
2562
  tcpData(id, bytes) {
1979
2563
  const t = this.tcps.get(id);
1980
2564
  const peer = t?.peer && this.tcps.get(t.peer);
1981
2565
  if (peer) this.run(() => peer.send(bytes), peer.h);
1982
2566
  else this.event({ t: 'tcp_data', id }, bytes);
2567
+ return !peer;
2568
+ }
2569
+
2570
+ // Data of a peer for the socket id, with the window win of Inbound: the
2571
+ // bytes that go to Erlang count, and the bytes to a spliced peer do not
2572
+ // (no tcp_read comes for them).
2573
+ tcpIn(id, win, bytes) {
2574
+ if (this.tcpData(id, bytes)) win.sent += bytes.length;
2575
+ }
2576
+
2577
+ // A send of the VM to the socket t. The host tells the VM how many bytes
2578
+ // the peer took (tcp_sent), after each ACK_BYTES: wasm_tcp holds a send
2579
+ // while SEND_WINDOW bytes wait in the host. A send that fails counts
2580
+ // too, because the socket then ends.
2581
+ tcpSend(id, t, body) {
2582
+ const done = () => {
2583
+ t.took = (t.took ?? 0) + body.length;
2584
+ if (t.took >= ACK_BYTES) {
2585
+ this.event({ t: 'tcp_sent', id, n: t.took });
2586
+ t.took = 0;
2587
+ }
2588
+ };
2589
+ let r;
2590
+ try {
2591
+ r = t.send(body);
2592
+ } finally {
2593
+ if (typeof r?.then !== 'function') done();
2594
+ }
2595
+ if (typeof r?.then === 'function') r.then(done, done);
1983
2596
  }
1984
2597
 
1985
2598
  // The end of a TCP socket: to Erlang, and the end of its peer too.
@@ -1995,10 +2608,22 @@ export class Vm {
1995
2608
  }
1996
2609
  }
1997
2610
 
2611
+ // A message of the VM (jspi_host_send). It runs in the thread of the VM:
2612
+ // an exception of the host must not stop that thread, so it goes to the
2613
+ // log.
1998
2614
  onsend(bytes) {
2615
+ try {
2616
+ this.onsendEvent(bytes);
2617
+ } catch (e) {
2618
+ this.log(`beam: the host failed on a message of the VM (${e?.message ?? e})`);
2619
+ }
2620
+ }
2621
+
2622
+ onsendEvent(bytes) {
1999
2623
  const nl = bytes.indexOf(10);
2000
2624
  const msg = JSON.parse(new TextDecoder().decode(bytes.subarray(0, nl)));
2001
- const body = bytes.slice(nl + 1);
2625
+ // bytes is a new array (jspi_host_send): the body is a view of it.
2626
+ const body = bytes.subarray(nl + 1);
2002
2627
  switch (msg.t) {
2003
2628
  case 'ready':
2004
2629
  this.onready();
@@ -2029,9 +2654,20 @@ export class Vm {
2029
2654
  case 'fetch_cancel':
2030
2655
  this.fetches.get(msg.id)?.abort();
2031
2656
  break;
2657
+ // wasm_host_fetch read n bytes of the body of a fetch().
2658
+ case 'fetch_read': {
2659
+ const ac = this.fetches.get(msg.id);
2660
+ if (ac) { ac.read += msg.n; ac.room?.(); }
2661
+ break;
2662
+ }
2032
2663
  case 'tcp_send': {
2033
2664
  const t = this.tcps.get(msg.id);
2034
- if (t) this.run(() => t.send(body), t.h);
2665
+ if (t) this.run(() => this.tcpSend(msg.id, t, body), t.h);
2666
+ break;
2667
+ }
2668
+ // wasm_tcp: the app read n bytes of a socket.
2669
+ case 'tcp_read': {
2670
+ this.tcps.get(msg.id)?.ack?.(msg.n);
2035
2671
  break;
2036
2672
  }
2037
2673
  case 'tcp_close': {
@@ -2115,7 +2751,195 @@ export function staticResponse(statics, request) {
2115
2751
  return new Response(body, { headers });
2116
2752
  }
2117
2753
 
2754
+ // The result of a wait that passed its time limit.
2755
+ const LATE = Symbol('late');
2756
+
2757
+ // A request body (bridgeUpload): the bytes that can be unread in the VM,
2758
+ // the smallest part of an event (except the last one), and the size of a
2759
+ // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2760
+ // costs a turn of the VM.
2761
+ const UPLOAD_WINDOW = 256 * 1024;
2762
+ const UPLOAD_PART = 16 * 1024;
2763
+ const UPLOAD_READ = 64 * 1024;
2764
+
2765
+ // The size bytes of the list parts, as one array.
2766
+ function join(parts, size) {
2767
+ const b = new Uint8Array(size);
2768
+ let at = 0;
2769
+ for (const p of parts) { b.set(p, at); at += p.length; }
2770
+ return b;
2771
+ }
2772
+
2773
+ // A chunk of a chunked body, and the end of the body.
2774
+ const CHUNKS_END = new TextEncoder().encode('0\r\n\r\n');
2775
+ function chunk(data) {
2776
+ const size = new TextEncoder().encode(`${data.length.toString(16)}\r\n`);
2777
+ const b = new Uint8Array(size.length + data.length + 2);
2778
+ b.set(size);
2779
+ b.set(data, size.length);
2780
+ b.set([13, 10], size.length + data.length);
2781
+ return b;
2782
+ }
2783
+
2784
+ // A limit of a var: a number above 0, else the default (0: no limit).
2785
+ // "0" turns a limit with a default off.
2786
+ function limit(text, fallback) {
2787
+ if (text === undefined || text === null || text === '') return fallback;
2788
+ const n = Number(text);
2789
+ return Number.isFinite(n) && n > 0 ? n : 0;
2790
+ }
2791
+
2118
2792
  // The position of the bytes pat in b, or -1.
2793
+ // The longest size line of a chunk of a response (with its extensions),
2794
+ // and the longest head of a response.
2795
+ const CHUNK_LINE = 4096;
2796
+ const HEAD_MAX = 1 << 20;
2797
+ // The longest wait of a request for a snapshot that the VM makes.
2798
+ const SNAPSHOT_WAIT = 10000;
2799
+ // Flow control of the sockets: wasm_tcp gives tcp_read after each
2800
+ // ACK_BYTES that the app read, and the host gives tcp_sent after each
2801
+ // ACK_BYTES that a peer took. Above SOCKET_QUEUE bytes from a peer that
2802
+ // wait for the app (Inbound), a WebSocket closes.
2803
+ const ACK_BYTES = 64 * 1024;
2804
+ const SOCKET_QUEUE = 16 * 1024 * 1024;
2805
+
2806
+ // The names of the fetch path in the cache of isCloudflare.
2807
+ const DNS_MAX = 1024;
2808
+
2809
+ // Bytes as text of one character for each byte (ISO-8859-1), and back.
2810
+ function latin1(b) {
2811
+ let s = '';
2812
+ for (let i = 0; i < b.length; i += 8192) s += String.fromCharCode(...b.subarray(i, i + 8192));
2813
+ return s;
2814
+ }
2815
+ function latin1Bytes(s) {
2816
+ const b = new Uint8Array(s.length);
2817
+ for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
2818
+ return b;
2819
+ }
2820
+
2821
+ // The close code of a client for the app: 1005, 1006 and 1015 are not
2822
+ // codes for the wire (RFC 6455).
2823
+ function wireCode(code) {
2824
+ if (code === 1006 || code === 1015) return 1001;
2825
+ return code && code !== 1005 ? code : 1000;
2826
+ }
2827
+
2828
+ // close(code) of a WebSocket, with a code that it takes (else 1000), and
2829
+ // no exception for a socket that is closed already.
2830
+ function closeSocket(ws, code) {
2831
+ try {
2832
+ ws.close(code === 1000 || (code >= 3000 && code <= 4999) || (code >= 1001 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ? code : 1000);
2833
+ } catch {}
2834
+ }
2835
+
2836
+ // Bytes in pieces (views of the arrays that came, no copy), joined only
2837
+ // when a reader needs them in one array (peek, take).
2838
+ // The bytes from a peer to a socket of the VM, with flow control: they go
2839
+ // while less than UPLOAD_WINDOW bytes are unread in the VM (win.sent, and
2840
+ // win.read from tcp_read), and the others wait here. Above SOCKET_QUEUE
2841
+ // bytes that wait, over() runs once, and the next bytes are dropped. A
2842
+ // forced push always waits (the end of a WebSocket).
2843
+ export class Inbound {
2844
+ constructor(win, send, over) {
2845
+ Object.assign(this, { win, send, over, queue: [], queued: 0, overflow: false });
2846
+ }
2847
+
2848
+ full() {
2849
+ return this.win.sent - this.win.read >= UPLOAD_WINDOW;
2850
+ }
2851
+
2852
+ push(bytes, force = false) {
2853
+ if (!this.queue.length && !this.full()) return this.send(bytes);
2854
+ if (!force && (this.overflow || this.queued + bytes.length > SOCKET_QUEUE)) {
2855
+ if (!this.overflow) {
2856
+ this.overflow = true;
2857
+ this.over();
2858
+ }
2859
+ return;
2860
+ }
2861
+ this.queue.push(bytes);
2862
+ this.queued += bytes.length;
2863
+ }
2864
+
2865
+ // After a tcp_read: the bytes that wait, while the window has room.
2866
+ flush() {
2867
+ while (this.queue.length && !this.full()) {
2868
+ const bytes = this.queue.shift();
2869
+ this.queued -= bytes.length;
2870
+ this.send(bytes);
2871
+ }
2872
+ }
2873
+ }
2874
+
2875
+ // The bytes from one socket of the VM to another one in the host (the
2876
+ // fetch path): deliver gives them to the other socket. A send ends when
2877
+ // the other socket read them (read, from its tcp_read), so the window of
2878
+ // the sender (tcp_sent) holds the bytes that wait. end() ends each send.
2879
+ export function relay(deliver) {
2880
+ let sent = 0, read = 0;
2881
+ const waits = [];
2882
+ return {
2883
+ send: (b) => {
2884
+ deliver(b);
2885
+ sent += b.length;
2886
+ if (read >= sent) return undefined;
2887
+ const mark = sent;
2888
+ return new Promise((resolve) => waits.push([mark, resolve]));
2889
+ },
2890
+ read: (n) => {
2891
+ read += n;
2892
+ while (waits.length && waits[0][0] <= read) waits.shift()[1]();
2893
+ },
2894
+ end: () => { for (const [, resolve] of waits.splice(0)) resolve(); },
2895
+ };
2896
+ }
2897
+
2898
+ export class Pieces {
2899
+ constructor() { this.list = []; this.size = 0; }
2900
+ push(b) {
2901
+ if (!b.length) return;
2902
+ this.list.push(b);
2903
+ this.size += b.length;
2904
+ }
2905
+ // The first n bytes in one array. A view when the first piece has them;
2906
+ // else a join, which then is the first piece.
2907
+ peek(n) {
2908
+ if (!this.list.length || this.list[0].length >= n) return (this.list[0] ?? new Uint8Array(0)).subarray(0, n);
2909
+ const out = new Uint8Array(n);
2910
+ let at = 0;
2911
+ for (const p of this.list) {
2912
+ if (at >= n) break;
2913
+ const k = Math.min(p.length, n - at);
2914
+ out.set(p.subarray(0, k), at);
2915
+ at += k;
2916
+ }
2917
+ this.drop(n);
2918
+ this.list.unshift(out);
2919
+ this.size += n;
2920
+ return out;
2921
+ }
2922
+ drop(n) {
2923
+ this.size -= n;
2924
+ while (n > 0) {
2925
+ const p = this.list[0];
2926
+ if (p.length <= n) { this.list.shift(); n -= p.length; }
2927
+ else { this.list[0] = p.subarray(n); n = 0; }
2928
+ }
2929
+ }
2930
+ take(n) {
2931
+ const b = this.peek(n);
2932
+ this.drop(n);
2933
+ return b;
2934
+ }
2935
+ // At most max bytes of the first piece (a view).
2936
+ next(max) {
2937
+ const b = this.list[0].subarray(0, Math.min(max, this.list[0].length));
2938
+ this.drop(b.length);
2939
+ return b;
2940
+ }
2941
+ }
2942
+
2119
2943
  function indexOf(b, pat) {
2120
2944
  outer: for (let i = 0; i + pat.length <= b.length; i++) {
2121
2945
  for (let j = 0; j < pat.length; j++) if (b[i + j] !== pat[j]) continue outer;