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.
- package/LICENSE +202 -0
- package/NOTICE +97 -0
- package/README.md +369 -0
- package/js/beam.com.mjs +33 -0
- package/js/download.mjs +78 -0
- package/js/node.mjs +75 -0
- package/package.json +36 -0
- package/runtime/app-com.js +203 -0
- package/runtime/beam.mjs +2 -0
- package/runtime/beam.wasm +0 -0
- package/runtime/browser/beam-wasm.js +2 -0
- package/runtime/browser/net.js +7 -0
- package/runtime/browser/none.js +4 -0
- package/runtime/browser.js +161 -0
- package/runtime/cloudflare/durable.js +361 -0
- package/runtime/cloudflare/index.js +76 -0
- package/runtime/cloudflare/release.js +42 -0
- package/runtime/cloudflare/snapshot.js +3 -0
- package/runtime/cloudflare/worker.js +1891 -0
- package/runtime/deno/app.js +8 -0
- package/runtime/deno/beam-wasm.js +3 -0
- package/runtime/deno/none.js +3 -0
- package/runtime/deno/release-bin.js +46 -0
- package/runtime/deno/snapshot-bin.js +11 -0
- package/runtime/deno/worker.js +1891 -0
- package/runtime/deno.js +346 -0
- package/runtime/deno.json +5 -0
- package/runtime/durable-global.js +40 -0
- package/runtime/durable.js +361 -0
- package/runtime/global.js +26 -0
- package/runtime/licenses/LICENSE +202 -0
- package/runtime/licenses/NOTICE +97 -0
- package/runtime/licenses/bcrypt_elixir.txt +87 -0
- package/runtime/licenses/cosmopolitan.txt +16 -0
- package/runtime/licenses/elixir/Apache-2.0.txt +73 -0
- package/runtime/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
- package/runtime/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
- package/runtime/licenses/emscripten.txt +102 -0
- package/runtime/licenses/exqlite.txt +21 -0
- package/runtime/licenses/llvm.txt +311 -0
- package/runtime/licenses/musl.txt +193 -0
- package/runtime/licenses/otp/BSD-2-Clause.txt +22 -0
- package/runtime/licenses/otp/BSD-3-Clause.txt +25 -0
- package/runtime/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
- package/runtime/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
- package/runtime/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
- package/runtime/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
- package/runtime/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
- package/runtime/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
- package/runtime/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
- package/runtime/licenses/otp/MIT.txt +17 -0
- package/runtime/licenses/otp/SGI-B-2.0.txt +25 -0
- package/runtime/licenses/otp/W3C-19980720.txt +55 -0
- package/runtime/licenses/picosat.txt +19 -0
- package/runtime/licenses/wamr.txt +219 -0
- package/runtime/nifs.js +2 -0
- package/runtime/package.json +1 -0
- package/runtime/page/404.html +49 -0
- package/runtime/page/app-com.js +203 -0
- package/runtime/page/beam.mjs +2 -0
- package/runtime/page/browser/beam-wasm.js +2 -0
- package/runtime/page/browser/net.js +7 -0
- package/runtime/page/browser/none.js +4 -0
- package/runtime/page/browser.js +161 -0
- package/runtime/page/index.html +50 -0
- package/runtime/page/licenses/LICENSE +202 -0
- package/runtime/page/licenses/NOTICE +97 -0
- package/runtime/page/licenses/bcrypt_elixir.txt +87 -0
- package/runtime/page/licenses/cosmopolitan.txt +16 -0
- package/runtime/page/licenses/elixir/Apache-2.0.txt +73 -0
- package/runtime/page/licenses/elixir/LicenseRef-elixir-trademark-policy.txt +98 -0
- package/runtime/page/licenses/elixir/LicenseRef-scancode-unicode.txt +58 -0
- package/runtime/page/licenses/emscripten.txt +102 -0
- package/runtime/page/licenses/exqlite.txt +21 -0
- package/runtime/page/licenses/llvm.txt +311 -0
- package/runtime/page/licenses/musl.txt +193 -0
- package/runtime/page/licenses/otp/BSD-2-Clause.txt +22 -0
- package/runtime/page/licenses/otp/BSD-3-Clause.txt +25 -0
- package/runtime/page/licenses/otp/LicenseRef-IETF-MIB.txt +9 -0
- package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS3.txt +23 -0
- package/runtime/page/licenses/otp/LicenseRef-RSA-PKCS5v2-0.txt +17 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-ietf-trust.txt +296 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-ietf.txt +23 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-w3c-docs-19990405.txt +26 -0
- package/runtime/page/licenses/otp/LicenseRef-scancode-wxwindows-free-doc-3.txt +57 -0
- package/runtime/page/licenses/otp/MIT.txt +17 -0
- package/runtime/page/licenses/otp/SGI-B-2.0.txt +25 -0
- package/runtime/page/licenses/otp/W3C-19980720.txt +55 -0
- package/runtime/page/licenses/picosat.txt +19 -0
- package/runtime/page/licenses/wamr.txt +219 -0
- package/runtime/page/main.js +176 -0
- package/runtime/page/runtime-id.js +3 -0
- package/runtime/page/scope.js +17 -0
- package/runtime/page/sw.js +111 -0
- package/runtime/page/vm.js +148 -0
- package/runtime/page/worker.js +1891 -0
- package/runtime/page/ws-shim.js +143 -0
- package/runtime/release/app.js +17 -0
- package/runtime/release.json +1 -0
- package/runtime/runtime-id.js +3 -0
- package/runtime/tcp-proxy.mjs +30 -0
- package/runtime/worker.js +1891 -0
package/runtime/deno.js
ADDED
|
@@ -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,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;
|