beam.com 0.1.0-rc.1

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.
Files changed (102) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +97 -0
  3. package/README.md +369 -0
  4. package/js/beam.com.mjs +33 -0
  5. package/js/download.mjs +78 -0
  6. package/js/node.mjs +75 -0
  7. package/package.json +36 -0
  8. package/runtime/app-com.js +203 -0
  9. package/runtime/beam.mjs +2 -0
  10. package/runtime/beam.wasm +0 -0
  11. package/runtime/browser/beam-wasm.js +2 -0
  12. package/runtime/browser/net.js +7 -0
  13. package/runtime/browser/none.js +4 -0
  14. package/runtime/browser.js +161 -0
  15. package/runtime/cloudflare/durable.js +361 -0
  16. package/runtime/cloudflare/index.js +76 -0
  17. package/runtime/cloudflare/release.js +42 -0
  18. package/runtime/cloudflare/snapshot.js +3 -0
  19. package/runtime/cloudflare/worker.js +1891 -0
  20. package/runtime/deno/app.js +8 -0
  21. package/runtime/deno/beam-wasm.js +3 -0
  22. package/runtime/deno/none.js +3 -0
  23. package/runtime/deno/release-bin.js +46 -0
  24. package/runtime/deno/snapshot-bin.js +11 -0
  25. package/runtime/deno/worker.js +1891 -0
  26. package/runtime/deno.js +346 -0
  27. package/runtime/deno.json +5 -0
  28. package/runtime/durable-global.js +40 -0
  29. package/runtime/durable.js +361 -0
  30. package/runtime/global.js +26 -0
  31. package/runtime/licenses/LICENSE +202 -0
  32. package/runtime/licenses/NOTICE +97 -0
  33. package/runtime/licenses/bcrypt_elixir.txt +87 -0
  34. package/runtime/licenses/cosmopolitan.txt +16 -0
  35. package/runtime/licenses/elixir/Apache-2.0.txt +73 -0
  36. package/runtime/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
  37. package/runtime/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
  38. package/runtime/licenses/emscripten.txt +102 -0
  39. package/runtime/licenses/exqlite.txt +21 -0
  40. package/runtime/licenses/llvm.txt +311 -0
  41. package/runtime/licenses/musl.txt +193 -0
  42. package/runtime/licenses/otp/BSD-2-Clause.txt +22 -0
  43. package/runtime/licenses/otp/BSD-3-Clause.txt +25 -0
  44. package/runtime/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
  45. package/runtime/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
  46. package/runtime/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
  47. package/runtime/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
  48. package/runtime/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
  49. package/runtime/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
  50. package/runtime/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
  51. package/runtime/licenses/otp/MIT.txt +17 -0
  52. package/runtime/licenses/otp/SGI-B-2.0.txt +25 -0
  53. package/runtime/licenses/otp/W3C-19980720.txt +55 -0
  54. package/runtime/licenses/picosat.txt +19 -0
  55. package/runtime/licenses/wamr.txt +219 -0
  56. package/runtime/nifs.js +2 -0
  57. package/runtime/package.json +1 -0
  58. package/runtime/page/404.html +49 -0
  59. package/runtime/page/app-com.js +203 -0
  60. package/runtime/page/beam.mjs +2 -0
  61. package/runtime/page/browser/beam-wasm.js +2 -0
  62. package/runtime/page/browser/net.js +7 -0
  63. package/runtime/page/browser/none.js +4 -0
  64. package/runtime/page/browser.js +161 -0
  65. package/runtime/page/index.html +50 -0
  66. package/runtime/page/licenses/LICENSE +202 -0
  67. package/runtime/page/licenses/NOTICE +97 -0
  68. package/runtime/page/licenses/bcrypt_elixir.txt +87 -0
  69. package/runtime/page/licenses/cosmopolitan.txt +16 -0
  70. package/runtime/page/licenses/elixir/Apache-2.0.txt +73 -0
  71. package/runtime/page/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
  72. package/runtime/page/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
  73. package/runtime/page/licenses/emscripten.txt +102 -0
  74. package/runtime/page/licenses/exqlite.txt +21 -0
  75. package/runtime/page/licenses/llvm.txt +311 -0
  76. package/runtime/page/licenses/musl.txt +193 -0
  77. package/runtime/page/licenses/otp/BSD-2-Clause.txt +22 -0
  78. package/runtime/page/licenses/otp/BSD-3-Clause.txt +25 -0
  79. package/runtime/page/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
  80. package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
  81. package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
  82. package/runtime/page/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
  83. package/runtime/page/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
  84. package/runtime/page/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
  85. package/runtime/page/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
  86. package/runtime/page/licenses/otp/MIT.txt +17 -0
  87. package/runtime/page/licenses/otp/SGI-B-2.0.txt +25 -0
  88. package/runtime/page/licenses/otp/W3C-19980720.txt +55 -0
  89. package/runtime/page/licenses/picosat.txt +19 -0
  90. package/runtime/page/licenses/wamr.txt +219 -0
  91. package/runtime/page/main.js +176 -0
  92. package/runtime/page/runtime-id.js +3 -0
  93. package/runtime/page/scope.js +17 -0
  94. package/runtime/page/sw.js +111 -0
  95. package/runtime/page/vm.js +148 -0
  96. package/runtime/page/worker.js +1891 -0
  97. package/runtime/page/ws-shim.js +143 -0
  98. package/runtime/release/app.js +17 -0
  99. package/runtime/release.json +1 -0
  100. package/runtime/runtime-id.js +3 -0
  101. package/runtime/tcp-proxy.mjs +30 -0
  102. package/runtime/worker.js +1891 -0
@@ -0,0 +1,346 @@
1
+ // The BEAM on Deno (Deno Deploy): worker.js of the Workers, with the parts of
2
+ // the Workers runtime that it uses given by Deno: the module imports of
3
+ // beam.wasm, release.bin and snapshot.bin (deno/worker.js, the copy of
4
+ // worker.js with the imports of deno/), WebSocketPair, caches.default, and
5
+ // a store for Ecto SQLite (Deno KV, or node:sqlite). The TCP sockets use
6
+ // node:net of Deno. There is no import map, so this file also runs from
7
+ // node_modules/beam.com/runtime/ (the npm package).
8
+ //
9
+ // A Deno isolate keeps its VM between requests, as a Durable Object does.
10
+ // So the VM runs as in a Durable Object (plain: false): its timers run
11
+ // between requests, and an app with Ecto SQLite makes its snapshot at the
12
+ // boot point, before its migrations.
13
+ //
14
+ // Ecto SQLite (BEAM_SQLITE):
15
+ // - unset or "kv": SQLite in the VM (exqlite of the runtime), with the
16
+ // pages of its databases in Deno KV (Deno.openKv(BEAM_KV)): all the
17
+ // isolates share them, and they stay after a deploy;
18
+ // - "memory", or the path of a file: node:sqlite in this isolate runs the
19
+ // SQL (a database for each isolate, or a file);
20
+ // - "off": SQLite in the VM, in its memory.
21
+ // When Deno KV does not open (no database for the app on Deno Deploy, or
22
+ // no --unstable-kv), "kv" becomes "memory".
23
+ //
24
+ // "beam.com INPUT -o DIR --target wasm32" writes deno.js, deno.json and
25
+ // deno/ into DIR, next to worker.js. In DIR:
26
+ // deno serve --allow-net --allow-read --allow-env --allow-write=/tmp deno.js
27
+ // or "deno deploy" with the entrypoint deno.js.
28
+ //
29
+ // A native app.com (beam.com INPUT -o app.com) in place of release.bin:
30
+ // its path is the first argument (deno serve -A deno.js app.com, or
31
+ // "args" of the "runtime" of Deno Deploy), else BEAM_APP. Then the same
32
+ // file runs natively and here, with the runtime of DIR or of the npm
33
+ // package (of the same beam.com: app-com.js checks it):
34
+ // deno serve -A node_modules/beam.com/runtime/deno.js app.com
35
+ // This file is also the module "beam.com" of the npm package in Deno (the
36
+ // condition "deno" of its exports), so the entry of a Worker runs here as
37
+ // it is (see cloudflare/index.js): "deno serve -A worker.js", with
38
+ // "unstable": ["kv", "raw-imports"] in deno.json for the import of
39
+ // app.com as bytes. serve(app) gives the fetch handler; the second
40
+ // argument of fetch is the info of deno serve. Beam is the Durable Object
41
+ // of Workers, and Deno does not use it: an isolate keeps its VM between
42
+ // requests, as a Durable Object does.
43
+
44
+ // WebSocketPair: Deno upgrades the request itself (Deno.upgradeWebSocket),
45
+ // and only with the request. The VM can make the pair outside the call of
46
+ // the request (a Durable Object runs the VM all the time). So the server
47
+ // end is a socket with no connection yet. fetch() connects it when it
48
+ // gets the response of the upgrade (upgrade).
49
+ const UPGRADE = Symbol('upgrade');
50
+ globalThis.WebSocketPair = class {
51
+ constructor() {
52
+ const listeners = { message: [], close: [], error: [] };
53
+ const queue = []; // the messages before the socket opens
54
+ let socket = null, closed = null;
55
+ const server = {
56
+ binaryType: 'arraybuffer',
57
+ accept() {},
58
+ addEventListener(type, f) { listeners[type]?.push(f); },
59
+ send(m) { if (socket?.readyState === 1) socket.send(m); else if (!closed) queue.push(m); },
60
+ close(code, reason) {
61
+ if (socket) { try { socket.close(code, reason); } catch {} } else closed = [code, reason];
62
+ },
63
+ };
64
+ const connect = (request) => {
65
+ const { socket: s, response } = Deno.upgradeWebSocket(request);
66
+ socket = s;
67
+ s.binaryType = server.binaryType;
68
+ for (const type of Object.keys(listeners)) {
69
+ s.addEventListener(type, (e) => { for (const f of listeners[type]) f(e); });
70
+ }
71
+ s.addEventListener('open', () => {
72
+ for (const m of queue.splice(0)) s.send(m);
73
+ if (closed) s.close(...closed);
74
+ });
75
+ return response;
76
+ };
77
+ this[0] = { [UPGRADE]: connect };
78
+ this[1] = server;
79
+ }
80
+ };
81
+
82
+ // new Response(null, { status: 101, webSocket }) of Workers: a response
83
+ // that fetch() changes to the response of the upgrade (Deno refuses the
84
+ // status 101 here).
85
+ const NativeResponse = Response;
86
+ globalThis.Response = class extends NativeResponse {
87
+ constructor(body, init) {
88
+ const connect = init?.webSocket?.[UPGRADE];
89
+ super(connect ? null : body, connect ? { status: 200 } : init);
90
+ if (connect) this[UPGRADE] = connect;
91
+ }
92
+ };
93
+
94
+ // The Cache API of Workers has caches.default.
95
+ if (globalThis.caches && !caches.default) {
96
+ try { caches.default = await caches.open('beam'); } catch {}
97
+ }
98
+
99
+ // Workers log a rejected promise that no code handles, and go on. Deno
100
+ // stops the process: so log it here, as Workers do.
101
+ addEventListener('unhandledrejection', (e) => {
102
+ e.preventDefault();
103
+ console.error('beam: unhandled rejection:', e.reason?.message ?? e.reason);
104
+ });
105
+
106
+ // The store of the SQLite files of the VM (HostFiles of worker.js) in
107
+ // Deno KV. The keys: [beam, sqlite, NAME, "m"] is the version and the size
108
+ // of the database NAME, and [beam, sqlite, NAME, "b", N, VERSION] is block N
109
+ // (4 KiB) as VERSION wrote it, with the version before it (p). A commit is
110
+ // one atomic operation with a check of the key "m".
111
+ // A local KV is a SQLite file. When two processes use it, an operation
112
+ // can fail with "database is locked": then the store tries it again.
113
+ class KvStore {
114
+ constructor(kv) { this.kv = kv; }
115
+ key(name, ...rest) { return ['beam', 'sqlite', name, ...rest]; }
116
+ meta(name) {
117
+ return retry(async () => {
118
+ const r = await this.kv.get(this.key(name, 'm'));
119
+ return { version: r.value?.version ?? 0, size: r.value?.size ?? 0, stamp: r.versionstamp };
120
+ });
121
+ }
122
+ // A block keeps its last two versions. When a newer version of the block
123
+ // names another version before it (p) than the one found, two later
124
+ // commits dropped the block of this version: the read fails (an I/O
125
+ // error of SQLite), and it does not give the data of another version.
126
+ block(name, n, version) {
127
+ return retry(async () => {
128
+ let found = null;
129
+ const it = this.kv.list({ start: this.key(name, 'b', n, 0), end: this.key(name, 'b', n, version + 1) },
130
+ { reverse: true, limit: 1 });
131
+ for await (const e of it) found = { data: e.value.d, v: e.key.at(-1), prev: e.value.p };
132
+ const newer = this.kv.list({ prefix: this.key(name, 'b', n), start: this.key(name, 'b', n, version + 1) },
133
+ { limit: 1 });
134
+ for await (const e of newer) {
135
+ if ((e.value.p || 0) !== (found?.v ?? 0)) {
136
+ throw new Error(`${name}: block ${n} of version ${version} is gone: the transaction is too old`);
137
+ }
138
+ }
139
+ return found;
140
+ });
141
+ }
142
+ // With release, the commit also removes the write lock. It does not
143
+ // check the lock: the version of the database is the check.
144
+ commit(name, stamp, { version, size, blocks }, release = false) {
145
+ return retry(async () => {
146
+ const op = this.kv.atomic().check({ key: this.key(name, 'm'), versionstamp: stamp });
147
+ for (const b of blocks) {
148
+ op.set(this.key(name, 'b', b.n, version), { d: b.data, p: b.prev });
149
+ if (b.drop) op.delete(this.key(name, 'b', b.n, b.drop));
150
+ }
151
+ op.set(this.key(name, 'm'), { version, size });
152
+ if (release) op.delete(this.key(name, 'w'));
153
+ const r = await op.commit();
154
+ return r.ok ? r.versionstamp : null;
155
+ });
156
+ }
157
+ // The write lock: a key with an owner and an end. The commit does not
158
+ // need it, because it checks the version of the database. The lease is
159
+ // the versionstamp of the lock. With no lock, one atomic operation
160
+ // takes it; else a read finds if the lock is ours or at its end.
161
+ lock(name, stamp, owner, ms) {
162
+ const m = this.key(name, 'm'), w = this.key(name, 'w');
163
+ const take = (mStamp, wStamp) => this.kv.atomic()
164
+ .check({ key: m, versionstamp: mStamp }).check({ key: w, versionstamp: wStamp })
165
+ .set(w, { owner, until: Date.now() + ms }, { expireIn: ms }).commit();
166
+ return retry(async () => {
167
+ let r = await take(stamp, null);
168
+ if (r.ok) return r.versionstamp;
169
+ const [mv, wv] = await this.kv.getMany([m, w]);
170
+ if (mv.versionstamp !== stamp || (wv.value && wv.value.owner !== owner && wv.value.until > Date.now())) return null;
171
+ r = await take(stamp, wv.versionstamp);
172
+ return r.ok ? r.versionstamp : null;
173
+ });
174
+ }
175
+ // One atomic operation: it removes the lock only when it is still that lease.
176
+ unlock(name, lease) {
177
+ const w = this.key(name, 'w');
178
+ return retry(() => this.kv.atomic().check({ key: w, versionstamp: lease }).delete(w).commit());
179
+ }
180
+ async remove(name) {
181
+ const keys = [];
182
+ for await (const e of this.kv.list({ prefix: this.key(name) })) keys.push(e.key);
183
+ for (let i = 0; i < keys.length; i += 500) {
184
+ const op = this.kv.atomic();
185
+ for (const k of keys.slice(i, i + 500)) op.delete(k);
186
+ await op.commit();
187
+ }
188
+ }
189
+ }
190
+
191
+ async function retry(fn) {
192
+ for (let i = 0; ; i++) {
193
+ try {
194
+ return await fn();
195
+ } catch (e) {
196
+ if (i >= 100 || !/database is locked/.test(e.message)) throw e;
197
+ await new Promise((r) => setTimeout(r, 5 + Math.random() * 20));
198
+ }
199
+ }
200
+ }
201
+
202
+ // Ecto SQLite in the host: the SQL storage of a Durable Object (exec, raw,
203
+ // columnNames), on node:sqlite.
204
+ class SqlStorage {
205
+ constructor(db) { this.db = db; }
206
+ exec(sql, ...params) {
207
+ const st = this.db.prepare(sql);
208
+ const args = params.map((p) => (p instanceof ArrayBuffer ? new Uint8Array(p) : p));
209
+ const columnNames = st.columns().map((c) => c.name);
210
+ let rows = [];
211
+ if (columnNames.length) {
212
+ st.setReturnArrays(true);
213
+ rows = st.all(...args);
214
+ } else {
215
+ st.run(...args);
216
+ }
217
+ const objects = () => rows.map((r) => Object.fromEntries(columnNames.map((c, i) => [c, r[i]])));
218
+ return {
219
+ columnNames,
220
+ raw: () => rows[Symbol.iterator](),
221
+ toArray: objects,
222
+ [Symbol.iterator]: () => objects()[Symbol.iterator](),
223
+ };
224
+ }
225
+ }
226
+
227
+ import { use } from './deno/app.js';
228
+
229
+ const { Vm } = await import('./deno/worker.js');
230
+ // The environment of the release, as the "vars" of a Worker: the
231
+ // variables of the process, without those of Deno and of the host. Some
232
+ // of those change for each isolate, and the key of a snapshot holds the
233
+ // environment. BEAM_ENV = "NAME,NAME" gives the exact list.
234
+ const HOST_VAR = /^(DENO_|OTEL_|K8S_|CDN_LOOP$)/;
235
+ const names = Deno.env.get('BEAM_ENV')?.split(',').map((n) => n.trim()).filter(Boolean);
236
+ const env = Object.fromEntries(Object.entries(Deno.env.toObject())
237
+ .filter(([k]) => (names ? names.includes(k) || k.startsWith('BEAM_') : !HOST_VAR.test(k))));
238
+ // The version of the deploy (as version_metadata of a Worker): a new
239
+ // deploy makes a new snapshot.
240
+ const deployment = Deno.env.get('DENO_DEPLOY_BUILD_ID');
241
+ if (deployment) env.BEAM_VERSION = { id: deployment };
242
+ // The host, for the app. The region of Deno Deploy changes for each
243
+ // isolate: so it goes to the VM after the boot point (vars), not into the
244
+ // key of the snapshot.
245
+ env.BEAM_HOST ??= Deno.env.get('DENO_DEPLOY') ? 'deno-deploy' : 'deno';
246
+ const region = Deno.env.get('DENO_REGION');
247
+ const vars = region ? { BEAM_REGION: region } : {};
248
+ let sql = null, files = null, kv = null;
249
+ try {
250
+ kv = await Deno.openKv(Deno.env.get('BEAM_KV'));
251
+ } catch (e) {
252
+ console.log(`beam: no Deno KV (${e.message})`);
253
+ }
254
+ let mode = env.BEAM_SQLITE ?? 'kv';
255
+ if (mode === 'kv') {
256
+ if (kv) files = new KvStore(kv);
257
+ else {
258
+ console.log('beam: no Deno KV: SQLite runs in node:sqlite, in memory');
259
+ mode = 'memory';
260
+ }
261
+ }
262
+ // The secrets that the VM makes (SECRET_KEY_BASE of a Phoenix app, see
263
+ // Vm.autoVars), in Deno KV: all the isolates share them. The first
264
+ // isolate that writes a value wins, and the others take that value.
265
+ const secrets = kv && {
266
+ async get(name) { return (await kv.get(['beam-secret', name])).value ?? undefined; },
267
+ async put(name, value) {
268
+ const key = ['beam-secret', name];
269
+ const r = await kv.atomic().check({ key, versionstamp: null }).set(key, value).commit();
270
+ return r.ok ? value : (await kv.get(key)).value;
271
+ },
272
+ };
273
+ if (mode !== 'kv' && mode !== 'off') {
274
+ const { DatabaseSync } = await import('node:sqlite');
275
+ sql = new SqlStorage(new DatabaseSync(mode === 'memory' ? ':memory:' : mode));
276
+ }
277
+ // The app can tell where its database is (kv, memory, off, or a path).
278
+ vars.BEAM_SQLITE = mode;
279
+ let vm; // the VM of this isolate
280
+
281
+ // The static assets of the app (static/, from wasm/erts/host/static.mjs),
282
+ // as the assets of a Worker: served before the VM.
283
+ const TYPES = {
284
+ css: 'text/css', js: 'text/javascript', mjs: 'text/javascript', json: 'application/json',
285
+ html: 'text/html; charset=utf-8', txt: 'text/plain; charset=utf-8', svg: 'image/svg+xml',
286
+ png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp',
287
+ ico: 'image/x-icon', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', map: 'application/json',
288
+ wasm: 'application/wasm',
289
+ };
290
+ const assets = new Set();
291
+ const STATIC = new URL('./static/', import.meta.url);
292
+ (function walk(dir, prefix) {
293
+ let entries;
294
+ try { entries = [...Deno.readDirSync(dir)]; } catch { return; }
295
+ for (const e of entries) {
296
+ if (e.isDirectory) walk(new URL(`${e.name}/`, dir), `${prefix}${e.name}/`);
297
+ else if (e.isFile) assets.add(`${prefix}${e.name}`);
298
+ }
299
+ })(STATIC, '/');
300
+
301
+ async function asset(request) {
302
+ if (request.method !== 'GET' && request.method !== 'HEAD') return null;
303
+ let path;
304
+ try { path = decodeURIComponent(new URL(request.url).pathname); } catch { return null; }
305
+ if (!assets.has(path)) return null;
306
+ const file = await Deno.open(new URL(`.${path}`, STATIC));
307
+ const type = TYPES[path.split('.').pop().toLowerCase()] ?? 'application/octet-stream';
308
+ return new Response(request.method === 'HEAD' ? null : file.readable, { headers: { 'content-type': type } });
309
+ }
310
+
311
+ const handler = {
312
+ async fetch(request, info) {
313
+ const file = await asset(request);
314
+ if (file) return file;
315
+ if (!vm) {
316
+ const v = vm = new Vm(env, { plain: false, sql, vars, files, secrets, host: new URL(request.url).hostname });
317
+ v.ready.catch(() => { if (vm === v) vm = undefined; });
318
+ }
319
+ // The scheme of the client (Deno gives it in the URL; Plug.SSL and
320
+ // force_ssl read x-forwarded-proto), and the address of the client
321
+ // (the VM reads cf-connecting-ip, as on Workers). A client sends
322
+ // these headers too: the values of Deno replace them.
323
+ const headers = new Headers(request.headers);
324
+ headers.set('x-forwarded-proto', new URL(request.url).protocol.slice(0, -1));
325
+ headers.delete('cf-connecting-ip');
326
+ const addr = info?.remoteAddr;
327
+ if (addr?.hostname) headers.set('cf-connecting-ip', addr.hostname);
328
+ const r = new Request(request, { headers });
329
+ // The upgrade needs the request that Deno gave.
330
+ return Promise.resolve(vm.fetch(r)).then((res) => (res?.[UPGRADE] ? res[UPGRADE](request) : res));
331
+ },
332
+ };
333
+
334
+ export default handler;
335
+
336
+ // The Durable Object of the entry of a Worker (export { Beam } from
337
+ // 'beam.com'): Deno does not use it.
338
+ export class Beam {}
339
+
340
+ // The engine for the app.com of the entry (the bytes of the file): the
341
+ // fetch handler of its VM. The options of Workers (binding, name) do not
342
+ // apply: one VM runs in each isolate.
343
+ export function serve(app) {
344
+ use(app);
345
+ return { fetch: (request, info) => handler.fetch(request, info) };
346
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "unstable": [
3
+ "kv"
4
+ ]
5
+ }
@@ -0,0 +1,40 @@
1
+ // durable.js with the release and a snapshot of the build in the Worker
2
+ // (release/release.bin, and release/snapshot.bin of wasm/snapshot/snapshot.mjs):
3
+ // the global scope of the isolate restores a spare VM, and the first
4
+ // Durable Object of the isolate takes it (Vm.adopt), so its first request
5
+ // only starts the threads. With BEAM_WARM (a path), a GET request of that
6
+ // path runs in the global scope first (see global.js). Another object in
7
+ // the same isolate restores the snapshot of the Cache API, or boots.
8
+ //
9
+ // Ecto SQLite: make the snapshot at the boot point
10
+ // (snapshot.mjs --boot-point), before the program and its migrations; the
11
+ // first request of each object then starts the program on its own storage.
12
+ //
13
+ // node wasm/snapshot/snapshot.mjs DIR --warm 4000:/ (or --boot-point)
14
+ // (cd DIR && wrangler deploy -c wrangler.durable-global.jsonc)
15
+ import { env } from 'cloudflare:workers';
16
+ import { Vm } from './worker.js';
17
+ import front, { Beam as Base } from './durable.js';
18
+ import release from './release/release.bin';
19
+ import snapshot from './release/snapshot.bin';
20
+
21
+ let spare = new Vm(env, { release, snapshot });
22
+ await spare.ready;
23
+ if (env.BEAM_WARM) await spare.warm(env.BEAM_WARM);
24
+
25
+ // The tenants and the instances of durable.js; the first object of the
26
+ // isolate takes the spare VM. With BEAM_PERSIST, each object boots its own
27
+ // VM: the spare VM booted with no storage, so it has not loaded the
28
+ // persisted files of the object.
29
+ export class Beam extends Base {
30
+ makeVm(vars, host) {
31
+ const sql = this.ctx.storage.sql, id = this.ctx.id.toString();
32
+ const take = spare && !this.env.BEAM_PERSIST;
33
+ const vm = take ? spare.adopt({ sql, id, vars })
34
+ : new Vm(this.env, { plain: false, sql, id, release, snapshot: null, vars, secrets: this.ctx.storage, host });
35
+ if (take) spare = null;
36
+ return vm;
37
+ }
38
+ }
39
+
40
+ export default front;