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

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,198 @@ 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
+ // With a body, the head goes in the event of the first part of the
1976
+ // body (bridgeUpload): a small body is one event, as before the parts.
1977
+ if (body) this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1978
+ else this.bridgeSend(c, bytes);
1670
1979
  });
1671
1980
  }
1672
1981
 
1982
+ // Bytes to the app on the connection c.
1983
+ bridgeSend(c, bytes) {
1984
+ c.sent += bytes.length;
1985
+ this.event({ t: 'tcp_data', id: c.id }, bytes);
1986
+ }
1987
+
1988
+ // The body of a request to the app, in parts of UPLOAD_PART bytes or more
1989
+ // (or the rest of the body): one event for each part, not for each small
1990
+ // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1991
+ // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1992
+ // the memory of the VM, and a slow app slows the client. The head of the
1993
+ // request goes with the bytes of the first read of the client, with no
1994
+ // wait for UPLOAD_PART bytes.
1995
+ async bridgeUpload(c, body, chunked, max, head) {
1996
+ // workerd has readAtLeast. The option min of the standard BYOB read is
1997
+ // not safe: when the stream closes with fewer bytes than min, the read
1998
+ // errors the stream (Node.js 26, Chromium). So the other hosts use the
1999
+ // default reader, and the parts come from the chunks here.
2000
+ let byob = null;
2001
+ try { byob = body.getReader({ mode: 'byob' }); } catch {}
2002
+ if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
2003
+ const reader = byob ?? body.getReader();
2004
+ const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2005
+ let total = 0;
2006
+ let part = [];
2007
+ let size = 0;
2008
+ // The gathered part to the app, when less than UPLOAD_WINDOW bytes are
2009
+ // unread. False when the connection ended while it waited.
2010
+ const flush = async () => {
2011
+ while (c.sent - c.read >= UPLOAD_WINDOW && !c.done && !this.dead) {
2012
+ await new Promise((resolve) => { c.room = resolve; });
2013
+ }
2014
+ if (c.done || this.dead) return false;
2015
+ const bytes = part.length === 1 ? part[0] : join(part, size);
2016
+ part = [];
2017
+ size = 0;
2018
+ out(chunked ? chunk(bytes) : bytes);
2019
+ return true;
2020
+ };
2021
+ // Bytes to the app, after the head when it did not go yet.
2022
+ const out = (bytes) => {
2023
+ clearTimeout(alone);
2024
+ this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
2025
+ head = null;
2026
+ };
2027
+ // A client that sends no body yet: the head goes alone after HEAD_WAIT
2028
+ // ms, so the app sees the request (and its own time limits run).
2029
+ const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
2030
+ try {
2031
+ for (;;) {
2032
+ // A read can end the stream with its last bytes (done and a value).
2033
+ const { value, done } = await read(head ? 1 : UPLOAD_PART);
2034
+ if (c.done || this.dead) break;
2035
+ if (value?.length) {
2036
+ total += value.length;
2037
+ if (max && total > max) {
2038
+ this.bridgeRefuse(c, this.tooLarge(max));
2039
+ break;
2040
+ }
2041
+ }
2042
+ // A chunk of the client can be large (Deno): it goes in pieces of
2043
+ // UPLOAD_READ bytes or less, so the window stays small.
2044
+ let ok = true;
2045
+ for (let at = 0; ok && value && at < value.length; at += UPLOAD_READ) {
2046
+ const piece = value.subarray(at, at + UPLOAD_READ);
2047
+ part.push(piece);
2048
+ size += piece.length;
2049
+ if (size >= UPLOAD_PART) ok = await flush();
2050
+ }
2051
+ if (ok && (done || head) && size) ok = await flush();
2052
+ if (!ok) break;
2053
+ if (done) {
2054
+ if (chunked) out(CHUNKS_END);
2055
+ else if (head) out(new Uint8Array(0));
2056
+ this.bridgeClock(c, c.timeout * 1000);
2057
+ return;
2058
+ }
2059
+ }
2060
+ reader.cancel().catch(() => {});
2061
+ } catch {
2062
+ // The client stopped the upload.
2063
+ this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2064
+ }
2065
+ }
2066
+
2067
+ // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
2068
+ bridgeClock(c, ms) {
2069
+ if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2070
+ }
2071
+
2072
+ // The app read n bytes of the connection c (tcp_read).
2073
+ bridgeRead(c, n) {
2074
+ c.read += n;
2075
+ this.bridgeRoom(c);
2076
+ c.inbound?.flush();
2077
+ }
2078
+
2079
+ // The upload of c waits no more (more room, or the end of c).
2080
+ bridgeRoom(c) {
2081
+ const room = c.room;
2082
+ c.room = null;
2083
+ room?.();
2084
+ }
2085
+
2086
+ // The answer of the host in place of the app (when the app has not
2087
+ // answered yet), and the end of the connection for the app.
2088
+ bridgeRefuse(c, response) {
2089
+ if (!c.status) {
2090
+ clearTimeout(c.timer);
2091
+ c.status = response.status;
2092
+ c.resolve(response);
2093
+ c.finished();
2094
+ }
2095
+ this.bridgeDone(c);
2096
+ }
2097
+
2098
+ // The answer above BEAM_MAX_BODY.
2099
+ tooLarge(max) {
2100
+ this.log(`beam: a request body above ${max} bytes: 413`);
2101
+ return new Response(`The body is larger than ${max} bytes.\n`, { status: 413, headers: { 'content-type': 'text/plain' } });
2102
+ }
2103
+
1673
2104
  // Bytes from the HTTP server of the app to the connection c of bridge().
1674
2105
  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]);
2106
+ c.in.push(data);
2107
+ while (!c.status) {
2108
+ // The head of the response: it is small, so the bytes are joined.
2109
+ const head = c.in.peek(c.in.size);
2110
+ const end = indexOf(head, [13, 10, 13, 10]);
2111
+ if (end < 0) {
2112
+ if (head.length > HEAD_MAX) throw new Error(`a head of a response above ${HEAD_MAX} bytes`);
2113
+ return;
2114
+ }
2115
+ // The bytes of a header value are characters of one byte (Headers
2116
+ // takes no character above U+00FF).
2117
+ const lines = latin1(head.subarray(0, end)).split('\r\n');
2118
+ c.in.drop(end + 4);
2119
+ const status = Number(lines[0].split(' ')[1]);
2120
+ // An informational head (100 Continue, 103 Early Hints): the head of
2121
+ // the response comes after it.
2122
+ if (status >= 100 && status < 200 && !(status === 101 && c.upgrade)) continue;
1684
2123
  const headers = new Headers();
1685
2124
  for (const l of lines.slice(1)) {
1686
2125
  const i = l.indexOf(':');
1687
2126
  headers.append(l.slice(0, i).trim(), l.slice(i + 1).trim());
1688
2127
  }
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) {
2128
+ clearTimeout(c.timer);
2129
+ if (status === 101) {
2130
+ c.status = 101;
2131
+ return this.bridgeUpgrade(c);
2132
+ }
2133
+ if (c.upgrade && status === 403) {
1692
2134
  console.log(`beam: the app refused the WebSocket of ${c.path} (403) from the origin ${c.origin}. ` +
1693
2135
  `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
2136
  }
@@ -1696,13 +2138,21 @@ export class Vm {
1696
2138
  c.chunked = /chunked/i.test(headers.get('transfer-encoding') ?? '');
1697
2139
  headers.delete('transfer-encoding');
1698
2140
  headers.delete('connection');
1699
- if (c.head || c.status === 204 || c.status === 304) c.length = 0;
2141
+ if (c.head || status === 204 || status === 304) { c.length = 0; c.chunked = false; }
2142
+ c.left = 0;
1700
2143
  const { readable, writable } = new TransformStream();
1701
- c.writer = writable.getWriter();
1702
2144
  // The app compressed the body (Bandit: gzip, deflate): the runtime
1703
2145
  // must send it as it is, not compress it again.
1704
2146
  const encodeBody = headers.has('content-encoding') ? 'manual' : 'automatic';
1705
- c.resolve(new Response(c.length === 0 ? null : readable, { status: c.status, headers, encodeBody }));
2147
+ // The Response first: a status that it refuses ends the connection
2148
+ // with 502 (bridgeGuard), and the client gets an answer.
2149
+ const response = new Response(c.length === 0 ? null : readable, { status, headers, encodeBody });
2150
+ c.status = status;
2151
+ c.writer = writable.getWriter();
2152
+ // The client went away (or the stream failed): the app gets the end
2153
+ // of the connection.
2154
+ c.writer.closed.catch(() => this.bridgeDone(c));
2155
+ c.resolve(response);
1706
2156
  c.finished();
1707
2157
  }
1708
2158
  if (c.ws) return this.bridgeFrames(c);
@@ -1710,32 +2160,95 @@ export class Vm {
1710
2160
  }
1711
2161
 
1712
2162
  // The body of a response: by content-length, chunked, or until the end.
2163
+ // The pieces of the app go to the stream as they are (views, no copy):
2164
+ // also the data of a large chunk, before its end.
1713
2165
  bridgeBody(c) {
1714
2166
  if (c.done) return;
2167
+ const pieces = c.in;
1715
2168
  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);
2169
+ while (pieces.size && !c.done) {
2170
+ if (c.left > 0) {
2171
+ const part = pieces.next(c.left);
2172
+ c.left -= part.length;
2173
+ this.bridgeWrite(c, part);
2174
+ if (c.left === 0) c.left = -2; // then the CRLF of the chunk
2175
+ continue;
2176
+ }
2177
+ if (c.left < 0) {
2178
+ const n = Math.min(-c.left, pieces.size);
2179
+ pieces.drop(n);
2180
+ c.left += n;
2181
+ continue;
2182
+ }
2183
+ // The size line of the next chunk (a chunk extension after ";").
2184
+ const line = pieces.peek(Math.min(pieces.size, CHUNK_LINE));
2185
+ const nl = line.indexOf(10);
2186
+ if (nl < 0) {
2187
+ if (line.length >= CHUNK_LINE) this.bridgeBad(c);
2188
+ return;
2189
+ }
2190
+ const size = parseInt(new TextDecoder().decode(line.subarray(0, nl)), 16);
2191
+ pieces.drop(nl + 1);
1720
2192
  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);
2193
+ if (!(size > 0)) return this.bridgeBad(c);
2194
+ c.left = size;
1724
2195
  }
2196
+ return;
1725
2197
  }
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());
2198
+ while (pieces.size && c.length !== 0) {
2199
+ const part = pieces.next(c.length ?? Infinity);
1729
2200
  if (c.length !== null) c.length -= part.length;
1730
- c.buf = new Uint8Array(0);
2201
+ this.bridgeWrite(c, part);
1731
2202
  }
1732
2203
  if (c.length === 0) this.bridgeDone(c);
1733
2204
  }
1734
2205
 
2206
+ // A part of the body to the client. A write to a stream that the client
2207
+ // closed fails, and writer.closed then ends the connection.
2208
+ bridgeWrite(c, part) {
2209
+ if (c.writer) c.wrote = c.writer.write(part).catch(() => {});
2210
+ }
2211
+
2212
+ // fn for the connection c (an event of the app). An exception ends c:
2213
+ // 502 before the head of the response, else the end of the stream or of
2214
+ // the WebSocket. Before, it stopped the thread of the VM, and so the VM.
2215
+ bridgeGuard(c, fn) {
2216
+ try {
2217
+ fn();
2218
+ } catch (e) {
2219
+ this.log(`beam: the response of ${c.path} failed (${e.message}): the connection ends`);
2220
+ if (c.ws) {
2221
+ try { c.ws.close(1011); } catch {}
2222
+ this.socketGone(c);
2223
+ this.conns.delete(c);
2224
+ this.tcps.delete(c.id);
2225
+ this.event({ t: 'tcp_closed', id: c.id });
2226
+ this.bridgeRelease(c);
2227
+ } else if (!c.status) {
2228
+ this.bridgeRefuse(c, new Response('bad gateway\n', { status: 502 }));
2229
+ } else {
2230
+ c.writer?.abort(e).catch(() => {});
2231
+ c.writer = null;
2232
+ this.bridgeDone(c);
2233
+ }
2234
+ }
2235
+ }
2236
+
2237
+ // A chunked body that is not HTTP/1.1: the stream of the response
2238
+ // fails, and the app gets the end of the connection.
2239
+ bridgeBad(c) {
2240
+ console.log(`beam: a bad chunked body from the app for ${c.path}: the response stops`);
2241
+ c.writer?.abort(new Error('a bad chunked body from the app')).catch(() => {});
2242
+ c.writer = null;
2243
+ this.bridgeDone(c);
2244
+ }
2245
+
1735
2246
  bridgeDone(c) {
1736
2247
  if (c.done) return;
1737
2248
  c.done = true;
2249
+ this.bridgeRoom(c);
1738
2250
  c.writer?.close().catch(() => {});
2251
+ this.conns.delete(c);
1739
2252
  this.tcps.delete(c.id);
1740
2253
  this.event({ t: 'tcp_closed', id: c.id });
1741
2254
  this.bridgeRelease(c);
@@ -1746,17 +2259,40 @@ export class Vm {
1746
2259
  if (c.released) return;
1747
2260
  c.released = true;
1748
2261
  if (c.h) { c.h.sockets--; c.h.wake?.(); }
2262
+ this.notePeak();
1749
2263
  }
1750
2264
 
1751
2265
  // The app closed the connection.
1752
2266
  bridgeEnd(c) {
2267
+ clearTimeout(c.timer);
1753
2268
  this.tcps.delete(c.id);
1754
2269
  if (!c.status) {
2270
+ c.status = 502;
1755
2271
  c.resolve(new Response('bad gateway\n', { status: 502 }));
1756
2272
  c.finished();
1757
2273
  }
1758
- if (c.ws) { try { c.ws.close(); } catch {} this.bridgeRelease(c); }
1759
- else this.bridgeDone(c);
2274
+ if (c.ws) {
2275
+ try { c.ws.close(); } catch {}
2276
+ this.socketGone(c);
2277
+ this.conns.delete(c);
2278
+ this.bridgeRelease(c);
2279
+ } else this.bridgeDone(c);
2280
+ }
2281
+
2282
+ // No head of a response in BEAM_REQUEST_TIMEOUT seconds: 504 for the
2283
+ // client, and the end of the connection for the app (tcp_closed).
2284
+ bridgeTimeout(c, seconds) {
2285
+ if (c.status || c.done) return;
2286
+ c.status = 504;
2287
+ c.resolve(this.late(`no response for ${c.path}`, seconds));
2288
+ c.finished();
2289
+ this.bridgeDone(c);
2290
+ }
2291
+
2292
+ // The answer of BEAM_REQUEST_TIMEOUT.
2293
+ late(what, seconds) {
2294
+ console.log(`beam: ${what} in ${seconds} s: 504`);
2295
+ return new Response('The app did not answer in time.\n', { status: 504, headers: { 'content-type': 'text/plain' } });
1760
2296
  }
1761
2297
 
1762
2298
  // A WebSocket: the client end to the browser, frames to the app.
@@ -1765,14 +2301,23 @@ export class Vm {
1765
2301
  server.accept();
1766
2302
  server.binaryType = 'arraybuffer';
1767
2303
  c.ws = server;
2304
+ c.counted = true;
2305
+ this.sockets++;
1768
2306
  c.frames = []; // the parts of a message in fragments
2307
+ // The messages of the client go while less than UPLOAD_WINDOW bytes
2308
+ // are unread in the VM; the others wait here (Inbound).
2309
+ c.inbound = new Inbound(c, (b) => this.bridgeSend(c, b), () => {
2310
+ console.log(`beam: the app reads the WebSocket of ${c.path} too slowly: it closes (1008)`);
2311
+ closeSocket(server, 1008);
2312
+ });
1769
2313
  server.addEventListener('message', (e) => {
1770
2314
  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)));
2315
+ c.inbound.push(frame(text ? 1 : 2, text ? new TextEncoder().encode(e.data) : new Uint8Array(e.data)));
1772
2316
  });
1773
2317
  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])));
2318
+ this.socketGone(c);
2319
+ const code = wireCode(e.code);
2320
+ c.inbound.push(frame(8, new Uint8Array([code >> 8, code & 255])), true);
1776
2321
  });
1777
2322
  c.resolve(new Response(null, { status: 101, webSocket: client }));
1778
2323
  c.finished();
@@ -1781,18 +2326,21 @@ export class Vm {
1781
2326
 
1782
2327
  // WebSocket frames from the app (not masked) to messages for the client.
1783
2328
  bridgeFrames(c) {
2329
+ const pieces = c.in;
1784
2330
  for (;;) {
1785
- const b = c.buf;
2331
+ // The head of a frame (2 to 10 bytes), then its payload in one array.
2332
+ const b = pieces.peek(Math.min(pieces.size, 10));
1786
2333
  if (b.length < 2) return;
1787
2334
  let len = b[1] & 127, at = 2;
1788
2335
  if (len === 126) { if (b.length < 4) return; len = (b[2] << 8) | b[3]; at = 4; }
1789
2336
  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);
2337
+ if (pieces.size < at + len) return;
2338
+ const fin = b[0] & 128, op = b[0] & 15;
2339
+ pieces.drop(at);
2340
+ const data = pieces.take(len);
1793
2341
  if (op === 9) { this.event({ t: 'tcp_data', id: c.id }, frame(10, data)); continue; }
1794
2342
  if (op === 10) continue;
1795
- if (op === 8) { c.ws.close(len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
2343
+ if (op === 8) { closeSocket(c.ws, len >= 2 ? (data[0] << 8) | data[1] : 1000); continue; }
1796
2344
  if (op !== 0) c.op = op;
1797
2345
  c.frames.push(data);
1798
2346
  if (!fin) continue;
@@ -1800,7 +2348,8 @@ export class Vm {
1800
2348
  let i = 0;
1801
2349
  for (const f of c.frames) { all.set(f, i); i += f.length; }
1802
2350
  c.frames = [];
1803
- c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all);
2351
+ // The client can close at any time: a send after it is dropped.
2352
+ try { c.ws.send(c.op === 1 ? new TextDecoder().decode(all) : all); } catch {}
1804
2353
  }
1805
2354
  }
1806
2355
 
@@ -1810,17 +2359,29 @@ export class Vm {
1810
2359
  const listener = this.listeners.get(port);
1811
2360
  finished();
1812
2361
  if (!listener) return new Response(`no listener on ${port}\n`, { status: 404 });
2362
+ const maxSockets = limit(this.env.BEAM_MAX_WEBSOCKETS, 0);
2363
+ if (maxSockets && this.sockets >= maxSockets) return this.busy('WebSockets');
1813
2364
  const id = `w${this.nextId++}`;
1814
2365
  const [client, server] = Object.values(new WebSocketPair());
1815
2366
  server.accept();
1816
2367
  server.binaryType = 'arraybuffer'; // (the default can be Blob)
1817
2368
  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 });
2369
+ const counted = { counted: true };
2370
+ this.sockets++;
2371
+ const t = { send: (b) => { try { server.send(b); } catch {} }, close: () => { this.socketGone(counted); try { server.close(); } catch {} }, h };
2372
+ const win = { sent: 0, read: 0 };
2373
+ const inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {
2374
+ console.log(`beam: the app reads the connection ${id} to port ${port} too slowly: it closes (1008)`);
2375
+ closeSocket(server, 1008);
2376
+ });
2377
+ t.ack = (n) => { win.read += n; inbound.flush(); };
2378
+ this.tcps.set(id, t);
2379
+ 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
2380
  server.addEventListener('message', (e) => {
1821
- this.tcpData(id, typeof e.data === 'string' ? new TextEncoder().encode(e.data) : e.data);
2381
+ inbound.push(typeof e.data === 'string' ? new TextEncoder().encode(e.data) : new Uint8Array(e.data));
1822
2382
  });
1823
2383
  server.addEventListener('close', () => {
2384
+ this.socketGone(counted);
1824
2385
  this.tcpClosed(id);
1825
2386
  if (h) { h.sockets--; h.wake?.(); }
1826
2387
  });
@@ -1833,7 +2394,7 @@ export class Vm {
1833
2394
  // first (fetchFirst); the other protocols use connect(). direct: the
1834
2395
  // tunnel of wasm_host_fetch, which never goes back to the fetch path.
1835
2396
  async tcpConnect({ id, host, port, direct }, h) {
1836
- let socket;
2397
+ let socket, inbound;
1837
2398
  if (h) h.sockets++;
1838
2399
  const allowed = connectAllowed(this.env.BEAM_CONNECT, host, port);
1839
2400
  const fetchPath = this.listeners.has('fetch') && !direct;
@@ -1843,11 +2404,21 @@ export class Vm {
1843
2404
  socket = net.connect({ host, port });
1844
2405
  // A send resolves when the bytes are written; a close sends the
1845
2406
  // bytes that wait, and then ends the socket.
1846
- this.tcps.set(id, {
2407
+ const t = {
1847
2408
  send: (b) => new Promise((resolve) => socket.write(b, () => resolve())),
1848
2409
  close: () => socket.end(() => socket.destroy()),
1849
2410
  h,
1850
- });
2411
+ };
2412
+ // The data of the peer goes while less than UPLOAD_WINDOW bytes are
2413
+ // unread in the VM; else the socket pauses.
2414
+ const win = { sent: 0, read: 0 };
2415
+ inbound = new Inbound(win, (b) => this.tcpIn(id, win, b), () => {});
2416
+ t.ack = (n) => {
2417
+ win.read += n;
2418
+ inbound.flush();
2419
+ if (!inbound.full()) socket.resume();
2420
+ };
2421
+ this.tcps.set(id, t);
1851
2422
  await new Promise((resolve, reject) => {
1852
2423
  socket.once('connect', resolve);
1853
2424
  socket.once('error', reject);
@@ -1869,9 +2440,12 @@ export class Vm {
1869
2440
  if (h) { h.sockets--; h.wake?.(); }
1870
2441
  return;
1871
2442
  }
1872
- this.event({ t: 'tcp_open', id });
2443
+ this.event({ t: 'tcp_open', id, ack: true, sent: true });
1873
2444
  await new Promise((resolve) => {
1874
- socket.on('data', (b) => this.tcpData(id, new Uint8Array(b.buffer, b.byteOffset, b.byteLength)));
2445
+ socket.on('data', (b) => {
2446
+ inbound.push(new Uint8Array(b.buffer, b.byteOffset, b.byteLength), true);
2447
+ if (inbound.full()) socket.pause();
2448
+ });
1875
2449
  socket.on('error', () => {});
1876
2450
  socket.once('close', resolve);
1877
2451
  });
@@ -1908,6 +2482,9 @@ export class Vm {
1908
2482
  } catch (e) {
1909
2483
  console.log(`beam: resolve ${name}: ${e.message}`);
1910
2484
  }
2485
+ // At most DNS_MAX names: a new name removes the oldest.
2486
+ this.dns.delete(name);
2487
+ if (this.dns.size >= DNS_MAX) this.dns.delete(this.dns.keys().next().value);
1911
2488
  this.dns.set(name, { at: now, addresses });
1912
2489
  return addresses.some((a) => inRanges(a));
1913
2490
  }
@@ -1917,21 +2494,29 @@ export class Vm {
1917
2494
  // an open connection to host:port. The pair ends when one side closes.
1918
2495
  async fetchPair(id, host, port, h) {
1919
2496
  const conn = `x${this.nextId++}`;
1920
- let done;
2497
+ let done, pairEnd;
1921
2498
  const ended = new Promise((r) => { done = r; });
1922
2499
  const end = (other) => {
1923
2500
  if (!this.fetchConns.has(conn)) return;
1924
2501
  this.fetchConns.delete(conn);
1925
2502
  this.tcps.delete(id);
1926
2503
  this.tcps.delete(conn);
2504
+ // The fetches of this connection: no one reads their bodies now.
2505
+ for (const ac of this.fetches.values()) if (ac.conn === conn) ac.abort();
2506
+ pairEnd();
1927
2507
  this.event({ t: 'tcp_closed', id: other });
1928
2508
  done();
1929
2509
  };
1930
2510
  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 });
2511
+ // A send of one socket ends when the other socket read the bytes
2512
+ // (tcp_read): so the window of the sender (tcp_sent) holds them.
2513
+ const toConn = relay((b) => this.event({ t: 'tcp_data', id: conn }, b));
2514
+ const toId = relay((b) => this.event({ t: 'tcp_data', id }, b));
2515
+ pairEnd = () => { toConn.end(); toId.end(); };
2516
+ this.tcps.set(id, { send: toConn.send, close: () => end(conn), ack: toId.read, h });
2517
+ this.tcps.set(conn, { send: toId.send, close: () => end(id), ack: toConn.read, h });
2518
+ this.event({ t: 'tcp_open', id, ack: true, sent: true });
2519
+ this.event({ t: 'tcp_accept', id: this.listeners.get('fetch'), conn, host, port, ack: true, sent: true });
1935
2520
  await ended;
1936
2521
  if (h) { h.sockets--; h.wake?.(); }
1937
2522
  }
@@ -1944,6 +2529,12 @@ export class Vm {
1944
2529
  if (h) h.sockets++;
1945
2530
  this.fetchPending++;
1946
2531
  const ac = new AbortController();
2532
+ ac.conn = msg.conn;
2533
+ // ack: wasm_host_fetch tells the host what it read (fetch_read). The
2534
+ // body then goes while less than UPLOAD_WINDOW bytes are unread.
2535
+ ac.sent = 0;
2536
+ ac.read = 0;
2537
+ ac.signal.addEventListener('abort', () => ac.room?.());
1947
2538
  this.fetches.set(msg.id, ac);
1948
2539
  try {
1949
2540
  const url = c && fetchUrl(c.host, c.port, msg.tls, msg.path);
@@ -1961,7 +2552,13 @@ export class Vm {
1961
2552
  for (;;) {
1962
2553
  const { done, value } = await reader.read();
1963
2554
  if (done) break;
1964
- if (value?.byteLength) this.event({ t: 'fetch_data', id: msg.id }, value);
2555
+ if (!value?.byteLength) continue;
2556
+ this.event({ t: 'fetch_data', id: msg.id }, value);
2557
+ ac.sent += value.byteLength;
2558
+ while (msg.ack && ac.sent - ac.read >= UPLOAD_WINDOW && !ac.signal.aborted) {
2559
+ await new Promise((resolve) => { ac.room = resolve; });
2560
+ }
2561
+ if (ac.signal.aborted) throw new Error('the fetch stopped');
1965
2562
  }
1966
2563
  }
1967
2564
  this.event({ t: 'fetch_end', id: msg.id });
@@ -1974,12 +2571,42 @@ export class Vm {
1974
2571
  }
1975
2572
  }
1976
2573
 
1977
- // Data of a TCP socket: to Erlang, or to its peer after a splice.
2574
+ // Data of a TCP socket: to Erlang (then true), or to its peer after a
2575
+ // splice.
1978
2576
  tcpData(id, bytes) {
1979
2577
  const t = this.tcps.get(id);
1980
2578
  const peer = t?.peer && this.tcps.get(t.peer);
1981
2579
  if (peer) this.run(() => peer.send(bytes), peer.h);
1982
2580
  else this.event({ t: 'tcp_data', id }, bytes);
2581
+ return !peer;
2582
+ }
2583
+
2584
+ // Data of a peer for the socket id, with the window win of Inbound: the
2585
+ // bytes that go to Erlang count, and the bytes to a spliced peer do not
2586
+ // (no tcp_read comes for them).
2587
+ tcpIn(id, win, bytes) {
2588
+ if (this.tcpData(id, bytes)) win.sent += bytes.length;
2589
+ }
2590
+
2591
+ // A send of the VM to the socket t. The host tells the VM how many bytes
2592
+ // the peer took (tcp_sent), after each ACK_BYTES: wasm_tcp holds a send
2593
+ // while SEND_WINDOW bytes wait in the host. A send that fails counts
2594
+ // too, because the socket then ends.
2595
+ tcpSend(id, t, body) {
2596
+ const done = () => {
2597
+ t.took = (t.took ?? 0) + body.length;
2598
+ if (t.took >= ACK_BYTES) {
2599
+ this.event({ t: 'tcp_sent', id, n: t.took });
2600
+ t.took = 0;
2601
+ }
2602
+ };
2603
+ let r;
2604
+ try {
2605
+ r = t.send(body);
2606
+ } finally {
2607
+ if (typeof r?.then !== 'function') done();
2608
+ }
2609
+ if (typeof r?.then === 'function') r.then(done, done);
1983
2610
  }
1984
2611
 
1985
2612
  // The end of a TCP socket: to Erlang, and the end of its peer too.
@@ -1995,10 +2622,22 @@ export class Vm {
1995
2622
  }
1996
2623
  }
1997
2624
 
2625
+ // A message of the VM (jspi_host_send). It runs in the thread of the VM:
2626
+ // an exception of the host must not stop that thread, so it goes to the
2627
+ // log.
1998
2628
  onsend(bytes) {
2629
+ try {
2630
+ this.onsendEvent(bytes);
2631
+ } catch (e) {
2632
+ this.log(`beam: the host failed on a message of the VM (${e?.message ?? e})`);
2633
+ }
2634
+ }
2635
+
2636
+ onsendEvent(bytes) {
1999
2637
  const nl = bytes.indexOf(10);
2000
2638
  const msg = JSON.parse(new TextDecoder().decode(bytes.subarray(0, nl)));
2001
- const body = bytes.slice(nl + 1);
2639
+ // bytes is a new array (jspi_host_send): the body is a view of it.
2640
+ const body = bytes.subarray(nl + 1);
2002
2641
  switch (msg.t) {
2003
2642
  case 'ready':
2004
2643
  this.onready();
@@ -2029,9 +2668,20 @@ export class Vm {
2029
2668
  case 'fetch_cancel':
2030
2669
  this.fetches.get(msg.id)?.abort();
2031
2670
  break;
2671
+ // wasm_host_fetch read n bytes of the body of a fetch().
2672
+ case 'fetch_read': {
2673
+ const ac = this.fetches.get(msg.id);
2674
+ if (ac) { ac.read += msg.n; ac.room?.(); }
2675
+ break;
2676
+ }
2032
2677
  case 'tcp_send': {
2033
2678
  const t = this.tcps.get(msg.id);
2034
- if (t) this.run(() => t.send(body), t.h);
2679
+ if (t) this.run(() => this.tcpSend(msg.id, t, body), t.h);
2680
+ break;
2681
+ }
2682
+ // wasm_tcp: the app read n bytes of a socket.
2683
+ case 'tcp_read': {
2684
+ this.tcps.get(msg.id)?.ack?.(msg.n);
2035
2685
  break;
2036
2686
  }
2037
2687
  case 'tcp_close': {
@@ -2115,7 +2765,198 @@ export function staticResponse(statics, request) {
2115
2765
  return new Response(body, { headers });
2116
2766
  }
2117
2767
 
2768
+ // The result of a wait that passed its time limit.
2769
+ const LATE = Symbol('late');
2770
+
2771
+ // A request body (bridgeUpload): the bytes that can be unread in the VM,
2772
+ // the smallest part of an event (except the last one), and the size of a
2773
+ // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2774
+ // costs a turn of the VM.
2775
+ const UPLOAD_WINDOW = 256 * 1024;
2776
+ const UPLOAD_PART = 32 * 1024;
2777
+ // The time that the head of a request waits for the first bytes of its
2778
+ // body (bridgeUpload), in ms.
2779
+ const HEAD_WAIT = 20;
2780
+ const UPLOAD_READ = 64 * 1024;
2781
+
2782
+ // The size bytes of the list parts, as one array.
2783
+ function join(parts, size) {
2784
+ const b = new Uint8Array(size);
2785
+ let at = 0;
2786
+ for (const p of parts) { b.set(p, at); at += p.length; }
2787
+ return b;
2788
+ }
2789
+
2790
+ // A chunk of a chunked body, and the end of the body.
2791
+ const CHUNKS_END = new TextEncoder().encode('0\r\n\r\n');
2792
+ function chunk(data) {
2793
+ const size = new TextEncoder().encode(`${data.length.toString(16)}\r\n`);
2794
+ const b = new Uint8Array(size.length + data.length + 2);
2795
+ b.set(size);
2796
+ b.set(data, size.length);
2797
+ b.set([13, 10], size.length + data.length);
2798
+ return b;
2799
+ }
2800
+
2801
+ // A limit of a var: a number above 0, else the default (0: no limit).
2802
+ // "0" turns a limit with a default off.
2803
+ function limit(text, fallback) {
2804
+ if (text === undefined || text === null || text === '') return fallback;
2805
+ const n = Number(text);
2806
+ return Number.isFinite(n) && n > 0 ? n : 0;
2807
+ }
2808
+
2118
2809
  // The position of the bytes pat in b, or -1.
2810
+ // The longest size line of a chunk of a response (with its extensions),
2811
+ // and the longest head of a response.
2812
+ const CHUNK_LINE = 4096;
2813
+ const HEAD_MAX = 1 << 20;
2814
+ // The longest wait of a request for a snapshot that the VM makes.
2815
+ const SNAPSHOT_WAIT = 10000;
2816
+ // Flow control of the sockets: wasm_tcp gives tcp_read after each
2817
+ // ACK_BYTES that the app read, and the host gives tcp_sent after each
2818
+ // ACK_BYTES that a peer took. Above SOCKET_QUEUE bytes from a peer that
2819
+ // wait for the app (Inbound), a WebSocket closes.
2820
+ const ACK_BYTES = 64 * 1024;
2821
+ const SOCKET_QUEUE = 16 * 1024 * 1024;
2822
+
2823
+ // The names of the fetch path in the cache of isCloudflare.
2824
+ const DNS_MAX = 1024;
2825
+
2826
+ // Bytes as text of one character for each byte (ISO-8859-1), and back.
2827
+ function latin1(b) {
2828
+ let s = '';
2829
+ for (let i = 0; i < b.length; i += 8192) s += String.fromCharCode(...b.subarray(i, i + 8192));
2830
+ return s;
2831
+ }
2832
+ function latin1Bytes(s) {
2833
+ const b = new Uint8Array(s.length);
2834
+ for (let i = 0; i < s.length; i++) b[i] = s.charCodeAt(i);
2835
+ return b;
2836
+ }
2837
+
2838
+ // The close code of a client for the app: 1005, 1006 and 1015 are not
2839
+ // codes for the wire (RFC 6455).
2840
+ function wireCode(code) {
2841
+ if (code === 1006 || code === 1015) return 1001;
2842
+ return code && code !== 1005 ? code : 1000;
2843
+ }
2844
+
2845
+ // close(code) of a WebSocket, with a code that it takes (else 1000), and
2846
+ // no exception for a socket that is closed already.
2847
+ function closeSocket(ws, code) {
2848
+ try {
2849
+ ws.close(code === 1000 || (code >= 3000 && code <= 4999) || (code >= 1001 && code <= 1014 && ![1004, 1005, 1006].includes(code)) ? code : 1000);
2850
+ } catch {}
2851
+ }
2852
+
2853
+ // Bytes in pieces (views of the arrays that came, no copy), joined only
2854
+ // when a reader needs them in one array (peek, take).
2855
+ // The bytes from a peer to a socket of the VM, with flow control: they go
2856
+ // while less than UPLOAD_WINDOW bytes are unread in the VM (win.sent, and
2857
+ // win.read from tcp_read), and the others wait here. Above SOCKET_QUEUE
2858
+ // bytes that wait, over() runs once, and the next bytes are dropped. A
2859
+ // forced push always waits (the end of a WebSocket).
2860
+ export class Inbound {
2861
+ constructor(win, send, over) {
2862
+ Object.assign(this, { win, send, over, queue: [], queued: 0, overflow: false });
2863
+ }
2864
+
2865
+ full() {
2866
+ return this.win.sent - this.win.read >= UPLOAD_WINDOW;
2867
+ }
2868
+
2869
+ push(bytes, force = false) {
2870
+ if (!this.queue.length && !this.full()) return this.send(bytes);
2871
+ if (!force && (this.overflow || this.queued + bytes.length > SOCKET_QUEUE)) {
2872
+ if (!this.overflow) {
2873
+ this.overflow = true;
2874
+ this.over();
2875
+ }
2876
+ return;
2877
+ }
2878
+ this.queue.push(bytes);
2879
+ this.queued += bytes.length;
2880
+ }
2881
+
2882
+ // After a tcp_read: the bytes that wait, while the window has room.
2883
+ flush() {
2884
+ while (this.queue.length && !this.full()) {
2885
+ const bytes = this.queue.shift();
2886
+ this.queued -= bytes.length;
2887
+ this.send(bytes);
2888
+ }
2889
+ }
2890
+ }
2891
+
2892
+ // The bytes from one socket of the VM to another one in the host (the
2893
+ // fetch path): deliver gives them to the other socket. A send ends when
2894
+ // the other socket read them (read, from its tcp_read), so the window of
2895
+ // the sender (tcp_sent) holds the bytes that wait. end() ends each send.
2896
+ export function relay(deliver) {
2897
+ let sent = 0, read = 0;
2898
+ const waits = [];
2899
+ return {
2900
+ send: (b) => {
2901
+ deliver(b);
2902
+ sent += b.length;
2903
+ if (read >= sent) return undefined;
2904
+ const mark = sent;
2905
+ return new Promise((resolve) => waits.push([mark, resolve]));
2906
+ },
2907
+ read: (n) => {
2908
+ read += n;
2909
+ while (waits.length && waits[0][0] <= read) waits.shift()[1]();
2910
+ },
2911
+ end: () => { for (const [, resolve] of waits.splice(0)) resolve(); },
2912
+ };
2913
+ }
2914
+
2915
+ export class Pieces {
2916
+ constructor() { this.list = []; this.size = 0; }
2917
+ push(b) {
2918
+ if (!b.length) return;
2919
+ this.list.push(b);
2920
+ this.size += b.length;
2921
+ }
2922
+ // The first n bytes in one array. A view when the first piece has them;
2923
+ // else a join, which then is the first piece.
2924
+ peek(n) {
2925
+ if (!this.list.length || this.list[0].length >= n) return (this.list[0] ?? new Uint8Array(0)).subarray(0, n);
2926
+ const out = new Uint8Array(n);
2927
+ let at = 0;
2928
+ for (const p of this.list) {
2929
+ if (at >= n) break;
2930
+ const k = Math.min(p.length, n - at);
2931
+ out.set(p.subarray(0, k), at);
2932
+ at += k;
2933
+ }
2934
+ this.drop(n);
2935
+ this.list.unshift(out);
2936
+ this.size += n;
2937
+ return out;
2938
+ }
2939
+ drop(n) {
2940
+ this.size -= n;
2941
+ while (n > 0) {
2942
+ const p = this.list[0];
2943
+ if (p.length <= n) { this.list.shift(); n -= p.length; }
2944
+ else { this.list[0] = p.subarray(n); n = 0; }
2945
+ }
2946
+ }
2947
+ take(n) {
2948
+ const b = this.peek(n);
2949
+ this.drop(n);
2950
+ return b;
2951
+ }
2952
+ // At most max bytes of the first piece (a view).
2953
+ next(max) {
2954
+ const b = this.list[0].subarray(0, Math.min(max, this.list[0].length));
2955
+ this.drop(b.length);
2956
+ return b;
2957
+ }
2958
+ }
2959
+
2119
2960
  function indexOf(b, pat) {
2120
2961
  outer: for (let i = 0; i + pat.length <= b.length; i++) {
2121
2962
  for (let j = 0; j < pat.length; j++) if (b[i + j] !== pat[j]) continue outer;