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.
package/runtime/beam.wasm CHANGED
Binary file
@@ -86,14 +86,15 @@ if (globalThis.caches && !caches.default) {
86
86
 
87
87
  // A request to the app. A Request of a page drops the headers Upgrade and
88
88
  // Sec-*, so the request is an object with the fields that worker.js reads,
89
- // and a Headers of its own.
89
+ // and a Headers of its own. The body is a stream, as the body of a Request
90
+ // (bridgeUpload of worker.js reads it in parts), with its size.
90
91
  function request(url, { method = 'GET', headers = {}, body = null } = {}) {
91
- return {
92
- url: url.href,
93
- method,
94
- headers: new Headers(headers),
95
- arrayBuffer: () => new NativeResponse(body).arrayBuffer(),
96
- };
92
+ const h = new Headers(headers);
93
+ if (body != null && !h.has('content-length')) {
94
+ const size = typeof body === 'string' ? new TextEncoder().encode(body).length : body.byteLength ?? body.size;
95
+ if (size !== undefined) h.set('content-length', String(size));
96
+ }
97
+ return { url: url.href, method, headers: h, body: body == null ? null : new NativeResponse(body).body };
97
98
  }
98
99
 
99
100
  // A reader of the bytes of the file of url (app-com.js): one range for each
@@ -44,6 +44,7 @@ export class Beam extends DurableObject {
44
44
  super(ctx, env);
45
45
  this.vm = null;
46
46
  this.expires = undefined;
47
+ this.resetting = false;
47
48
  }
48
49
 
49
50
  // The VM starts at the first request: a path tenant gives its name there.
@@ -55,6 +56,10 @@ export class Beam extends DurableObject {
55
56
  }
56
57
 
57
58
  async fetch(request) {
59
+ if (this.resetting) {
60
+ return new Response('The app stopped. Try again.\n',
61
+ { status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
62
+ }
58
63
  if (this.env.BEAM_INSTANCES && this.env.BEAM_TENANTS === 'path') {
59
64
  this.expires ??= (await this.ctx.storage.get(EXPIRES)) ?? null;
60
65
  if (!this.expires || this.expires <= Date.now()) {
@@ -71,11 +76,36 @@ export class Beam extends DurableObject {
71
76
  vars.BEAM_TENANT_PATH = `/t/${tenant}`;
72
77
  }
73
78
  if (this.expires) vars.BEAM_INSTANCE_EXPIRES = String(Math.floor(this.expires / 1000));
74
- this.vm = this.makeVm(vars, new URL(request.url).hostname);
79
+ const vm = this.vm = this.makeVm(vars, new URL(request.url).hostname);
80
+ vm.onDead = () => this.vmStopped(vm);
81
+ vm.ready.catch(() => this.vmStopped(vm));
75
82
  }
76
83
  return this.vm.fetch(request);
77
84
  }
78
85
 
86
+ // The VM stopped (erlang:halt, or a trap such as an allocation that
87
+ // failed), or its boot failed. Its open requests got 503. The memory of
88
+ // WebAssembly does not shrink, so the object resets after these answers:
89
+ // a new instance of the object, with a new VM, takes the next request.
90
+ vmStopped(vm) {
91
+ if (this.vm !== vm) return;
92
+ this.vm = null;
93
+ this.resetting = true;
94
+ setTimeout(() => this.reset('the VM stopped'), 100);
95
+ }
96
+
97
+ // A new instance of the object. When the host cannot abort the object,
98
+ // the next request starts a new VM in this instance: no request stays
99
+ // at 503.
100
+ reset(reason) {
101
+ try {
102
+ this.ctx.abort(reason);
103
+ } catch (e) {
104
+ console.log(`beam: the object did not reset (${e.message}): the next request starts a new VM`);
105
+ this.resetting = false;
106
+ }
107
+ }
108
+
79
109
  // An instance: its time limit (from the registry).
80
110
  async start(expires) {
81
111
  await this.ctx.storage.put(EXPIRES, expires);
@@ -92,7 +122,7 @@ export class Beam extends DurableObject {
92
122
  async expire() {
93
123
  this.expires = null;
94
124
  await this.ctx.storage.deleteAll();
95
- if (this.vm) setTimeout(() => this.ctx.abort('the instance expired'), 0);
125
+ if (this.vm) setTimeout(() => this.reset('the instance expired'), 0);
96
126
  }
97
127
 
98
128
  // The registry (the object ".registry") admits a visitor: a ticket and a
@@ -27,7 +27,7 @@
27
27
  import { env as globalEnv } from 'cloudflare:workers';
28
28
  import { release, use } from './release.js';
29
29
  import { useSnapshot } from './snapshot.js';
30
- import plain, { snapshotHeader, staticResponse, Vm } from './worker.js';
30
+ import plain, { parseSnapshot, staticResponse, Vm } from './worker.js';
31
31
  import front from './durable.js';
32
32
 
33
33
  export { Beam } from './durable.js';
@@ -62,7 +62,9 @@ export { Beam } from './durable.js';
62
62
  // --snapshot app.com, imported as a Data module, as app.com):
63
63
  // - a full one (--full): each new VM restores it in place of a boot. A
64
64
  // stateless Worker restores its VM in the global scope, before the
65
- // first request;
65
+ // first request. The global scope cannot inflate the pages of a
66
+ // snapshot in gzip (BEAMSNZ1), so such a snapshot restores in the first
67
+ // request;
66
68
  // - one at the boot point: a new VM restores it only when the store of
67
69
  // snapshots has none (the first VM of a deploy), and then the VM makes
68
70
  // its snapshot for the store, as with no snapshot of the build.
@@ -80,12 +82,21 @@ export function serve(app, { binding = 'BEAM', name, nifs = null, snapshot = nul
80
82
  // The VM of a stateless Worker, restored from a full snapshot in the
81
83
  // global scope.
82
84
  let vm = null;
83
- const full = !!snapshot && !snapshotHeader(snapshot).boot_point;
85
+ const parsed = snapshot && parseSnapshot(snapshot);
86
+ const full = !!parsed && !parsed.boot_point && !parsed.packed;
84
87
  const ready = full && !globalEnv[binding] ? (async () => {
85
88
  vm = new Vm(globalEnv, { release: await release(), snapshot });
89
+ // A VM that stopped: the requests then go to the Worker of worker.js,
90
+ // which boots a VM of its own.
91
+ vm.onDead = () => { vm = null; };
86
92
  await vm.ready;
87
93
  if (globalEnv.BEAM_WARM) await vm.warm(globalEnv.BEAM_WARM);
88
- })() : Promise.resolve();
94
+ })().catch((e) => {
95
+ // A VM that did not restore: the same, and the entry still loads
96
+ // (await beam.ready).
97
+ console.log(`beam: the snapshot of the build did not restore in the global scope (${e?.message ?? e}): each request boots a VM`);
98
+ vm = null;
99
+ }) : Promise.resolve();
89
100
  // The env of the front: the binding as BEAM, the name of the object, and
90
101
  // the static files of app.com (the front serves them, with no request
91
102
  // to the object).