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.
- package/README.md +5 -3
- package/js/beam.com.mjs +37 -6
- package/js/download.mjs +31 -4
- package/js/node.mjs +8 -3
- package/js/wrangler.mjs +47 -0
- package/package.json +1 -1
- package/runtime/beam.mjs +1 -1
- package/runtime/beam.wasm +0 -0
- package/runtime/browser.js +8 -7
- package/runtime/cloudflare/durable.js +32 -2
- package/runtime/cloudflare/index.js +15 -4
- package/runtime/cloudflare/worker.js +924 -83
- package/runtime/deno/worker.js +924 -83
- package/runtime/deno.js +2 -0
- package/runtime/durable.js +32 -2
- package/runtime/global.js +6 -3
- package/runtime/page/beam.mjs +1 -1
- package/runtime/page/browser.js +8 -7
- package/runtime/page/runtime-id.js +1 -1
- package/runtime/page/worker.js +924 -83
- package/runtime/release.json +1 -1
- package/runtime/runtime-id.js +1 -1
- package/runtime/worker.js +924 -83
package/README.md
CHANGED
|
@@ -119,7 +119,9 @@ cd hello && ../mix.com deps.get && ../mix.com ecto.migrate
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
One file runs natively and in the WebAssembly runtime. Each executable of
|
|
122
|
-
`-o` also has its edge part, of 40 to 180 KB (`--no-edge` leaves it out
|
|
122
|
+
`-o` also has its edge part, of 40 to 180 KB (`--no-edge` leaves it out,
|
|
123
|
+
and `--target wasm32 -o FILE.com` writes only the edge part, with no native
|
|
124
|
+
program).
|
|
123
125
|
A release directory of `mix release` is an input too:
|
|
124
126
|
|
|
125
127
|
```sh
|
|
@@ -217,7 +219,7 @@ jobs:
|
|
|
217
219
|
|
|
218
220
|
Then turn on Pages one time: Settings, Pages, Source "GitHub Actions". The
|
|
219
221
|
`GITHUB_TOKEN` cannot do this step. Until the release `v0.1.0`, use the
|
|
220
|
-
release candidate `@v0.1.0-rc.
|
|
222
|
+
release candidate `@v0.1.0-rc.4` in place of `@v0.1.0`. With `@main`, the
|
|
221
223
|
workflow uses the `edge` build of `beam.com`.
|
|
222
224
|
|
|
223
225
|
The workflow ([`pages-app.yml`](.github/workflows/pages-app.yml)) builds
|
|
@@ -284,7 +286,7 @@ Each GitHub release from 0.1.0-rc.2 also holds the package, the same
|
|
|
284
286
|
file as on npm. npm installs it with no registry:
|
|
285
287
|
|
|
286
288
|
```sh
|
|
287
|
-
npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.
|
|
289
|
+
npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.4/beam.com-0.1.0-rc.4.tgz
|
|
288
290
|
```
|
|
289
291
|
|
|
290
292
|
- `BEAM_COM`: the path of a `beam.com` to use. Then nothing is
|
package/js/beam.com.mjs
CHANGED
|
@@ -3,11 +3,15 @@
|
|
|
3
3
|
// (download.mjs). For example: npx beam.com app.erl -o app.com
|
|
4
4
|
//
|
|
5
5
|
// npx beam.com --snapshot APP.com [-o FILE] [--full] [--warm PATH]...
|
|
6
|
-
// [--env NAME=VALUE]
|
|
7
|
-
// serve(app, { snapshot }) of a Worker (snapshot() of
|
|
8
|
-
// (default APP.snapshot)
|
|
6
|
+
// [--env NAME=VALUE]... [--no-compress]: the snapshot of the build of
|
|
7
|
+
// APP.com for serve(app, { snapshot }) of a Worker (snapshot() of
|
|
8
|
+
// node.mjs), in FILE (default APP.snapshot), with its pages in gzip (not
|
|
9
|
+
// with --full: the global scope of a Worker cannot inflate). Give
|
|
10
|
+
// the BEAM_ERL_FLAGS of the Worker with --env, or in the vars of
|
|
11
|
+
// wrangler.jsonc of this directory: a Worker with other flags boots in
|
|
12
|
+
// place of the snapshot. It runs here, with no download of beam.com.
|
|
9
13
|
import { spawn } from 'node:child_process';
|
|
10
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { closeSync, openSync, readFileSync, readSync, statSync, writeFileSync } from 'node:fs';
|
|
11
15
|
import { constants } from 'node:os';
|
|
12
16
|
import { command, ensure } from './download.mjs';
|
|
13
17
|
|
|
@@ -32,6 +36,7 @@ async function snapshotCommand(args) {
|
|
|
32
36
|
};
|
|
33
37
|
if (a === '-o') out = value();
|
|
34
38
|
else if (a === '--full') opts.kind = 'full';
|
|
39
|
+
else if (a === '--no-compress') opts.compress = false;
|
|
35
40
|
else if (a === '--warm') opts.warm.push(value());
|
|
36
41
|
else if (a === '--env') {
|
|
37
42
|
const v = value(), at = v.indexOf('=');
|
|
@@ -40,14 +45,40 @@ async function snapshotCommand(args) {
|
|
|
40
45
|
} else if (!a.startsWith('-') && !app) app = a;
|
|
41
46
|
else throw new Error(`--snapshot: unknown argument ${a}`);
|
|
42
47
|
}
|
|
43
|
-
if (!app) throw new Error('usage: npx beam.com --snapshot APP.com [-o FILE] [--full] [--warm PATH]... [--env NAME=VALUE]...');
|
|
48
|
+
if (!app) throw new Error('usage: npx beam.com --snapshot APP.com [-o FILE] [--full] [--warm PATH]... [--env NAME=VALUE]... [--no-compress]');
|
|
44
49
|
if (opts.warm.length && opts.kind !== 'full') throw new Error('--warm is for a full snapshot (--full)');
|
|
45
50
|
out ??= app.replace(/\.com$/, '') + '.snapshot';
|
|
51
|
+
// The flags of the Worker, when --env does not give them: the vars of
|
|
52
|
+
// its wrangler.jsonc. A Worker with other flags boots in place of the
|
|
53
|
+
// snapshot.
|
|
54
|
+
let source = '--env';
|
|
55
|
+
if (opts.env.BEAM_ERL_FLAGS === undefined) {
|
|
56
|
+
const { wranglerVars } = await import('./wrangler.mjs');
|
|
57
|
+
const w = wranglerVars('.');
|
|
58
|
+
if (typeof w?.vars.BEAM_ERL_FLAGS === 'string') {
|
|
59
|
+
opts.env.BEAM_ERL_FLAGS = w.vars.BEAM_ERL_FLAGS;
|
|
60
|
+
source = w.file;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
46
63
|
const { snapshot } = await import('./node.mjs');
|
|
47
64
|
const t0 = performance.now();
|
|
48
65
|
const bytes = await snapshot(app, opts);
|
|
49
66
|
writeFileSync(out, bytes);
|
|
50
|
-
|
|
67
|
+
const flags = (opts.env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
|
|
68
|
+
console.error(`beam.com: wrote ${out} (${(bytes.length / 1048576).toFixed(1)} MB, ${opts.kind}, `
|
|
69
|
+
+ `BEAM_ERL_FLAGS "${flags}"${flags ? ` of ${source}` : ''}, in ${Math.round(performance.now() - t0)} ms)`);
|
|
70
|
+
if (!flags) console.error('beam.com: a Worker with BEAM_ERL_FLAGS boots in place of this snapshot: give its flags with --env BEAM_ERL_FLAGS=..., or in the vars of wrangler.jsonc');
|
|
71
|
+
// A Worker can be at most 64 MiB, with all its modules (gzip does not
|
|
72
|
+
// count): the app file and the snapshot are most of it.
|
|
73
|
+
const total = statSync(app).size + bytes.length;
|
|
74
|
+
if (total > 56 * 1048576) {
|
|
75
|
+
// An APE file starts with "MZ"; a file of --target wasm32 is a zip.
|
|
76
|
+
const head = Buffer.alloc(2), fd = openSync(app, 'r');
|
|
77
|
+
try { readSync(fd, head, 0, 2, 0); } finally { closeSync(fd); }
|
|
78
|
+
const native = head.toString() === 'MZ';
|
|
79
|
+
console.error(`beam.com: warning: ${app} and ${out} are ${(total / 1048576).toFixed(1)} MiB, and a Worker can be at most 64 MiB`
|
|
80
|
+
+ (native ? `. Build the app with -o FILE.com --target wasm32: a file with no native program, about 25 MB smaller` : ''));
|
|
81
|
+
}
|
|
51
82
|
}
|
|
52
83
|
|
|
53
84
|
let release = null;
|
package/js/download.mjs
CHANGED
|
@@ -14,6 +14,9 @@ import path from 'node:path';
|
|
|
14
14
|
|
|
15
15
|
export const REPOSITORY = 'sntran/BEAM.com';
|
|
16
16
|
|
|
17
|
+
// The time with no bytes that stops a download (ms).
|
|
18
|
+
const IDLE = 60000;
|
|
19
|
+
|
|
17
20
|
// The cache directory of beam.com (beam_com:cache_dir/0).
|
|
18
21
|
export function cacheDir(env = process.env) {
|
|
19
22
|
if (env.BEAM_COM_CACHE) return env.BEAM_COM_CACHE;
|
|
@@ -37,7 +40,8 @@ export function downloadUrl({ version }, env = process.env) {
|
|
|
37
40
|
|
|
38
41
|
// The path of a beam.com that runs. The file in the cache was checked when
|
|
39
42
|
// it was downloaded: a file is in the cache only after the check.
|
|
40
|
-
|
|
43
|
+
// idle: the download stops when no bytes come in this time (ms).
|
|
44
|
+
export async function ensure(release, { env = process.env, platform = process.platform, fetch = globalThis.fetch, log = () => {}, idle = IDLE } = {}) {
|
|
41
45
|
if (env.BEAM_COM) return env.BEAM_COM;
|
|
42
46
|
if (!release?.version || !/^[0-9a-f]{64}$/.test(release?.sha256 ?? '')) {
|
|
43
47
|
throw new Error('this package has no release of beam.com (runtime/release.json): set BEAM_COM to the path of a beam.com');
|
|
@@ -46,16 +50,36 @@ export async function ensure(release, { env = process.env, platform = process.pl
|
|
|
46
50
|
if (fs.existsSync(file)) return file;
|
|
47
51
|
const url = downloadUrl(release, env);
|
|
48
52
|
log(`beam.com: downloading ${url}`);
|
|
49
|
-
const
|
|
50
|
-
|
|
53
|
+
const ac = new AbortController();
|
|
54
|
+
let timer;
|
|
55
|
+
const wait = () => {
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = setTimeout(() => ac.abort(new Error(`beam.com: ${url}: no bytes in ${idle / 1000} s`)), idle);
|
|
58
|
+
};
|
|
59
|
+
wait();
|
|
60
|
+
let response;
|
|
61
|
+
try {
|
|
62
|
+
response = await fetch(url, { signal: ac.signal });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
throw ac.signal.aborted ? ac.signal.reason : e;
|
|
66
|
+
}
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
throw new Error(`beam.com: ${url}: status ${response.status}`);
|
|
70
|
+
}
|
|
51
71
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
72
|
const part = `${file}.${process.pid}.part`;
|
|
53
73
|
const out = fs.createWriteStream(part, { mode: 0o755 });
|
|
74
|
+
// An error of the file (a full disk): no drain comes after it.
|
|
75
|
+
const failed = new Promise((_, reject) => out.once('error', reject));
|
|
76
|
+
failed.catch(() => {});
|
|
54
77
|
const hash = createHash('sha256');
|
|
55
78
|
try {
|
|
56
79
|
for await (const chunk of response.body) {
|
|
80
|
+
wait();
|
|
57
81
|
hash.update(chunk);
|
|
58
|
-
if (!out.write(chunk)) await new Promise((r) => out.once('drain', r));
|
|
82
|
+
if (!out.write(chunk)) await Promise.race([new Promise((r) => out.once('drain', r)), failed]);
|
|
59
83
|
}
|
|
60
84
|
await new Promise((resolve, reject) => out.end((e) => (e ? reject(e) : resolve())));
|
|
61
85
|
const got = hash.digest('hex');
|
|
@@ -64,7 +88,10 @@ export async function ensure(release, { env = process.env, platform = process.pl
|
|
|
64
88
|
}
|
|
65
89
|
fs.chmodSync(part, 0o755);
|
|
66
90
|
fs.renameSync(part, file);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
throw ac.signal.aborted ? ac.signal.reason : e;
|
|
67
93
|
} finally {
|
|
94
|
+
clearTimeout(timer);
|
|
68
95
|
out.destroy();
|
|
69
96
|
fs.rmSync(part, { force: true });
|
|
70
97
|
}
|
package/js/node.mjs
CHANGED
|
@@ -86,11 +86,16 @@ export async function boot(app, { env = {}, statics = true } = {}) {
|
|
|
86
86
|
// It holds the variables of env and the state of the program. A Phoenix
|
|
87
87
|
// app must give SECRET_KEY_BASE and PHX_HOST, and an app with Ecto
|
|
88
88
|
// SQLite cannot use it (its boot changes the database).
|
|
89
|
+
// The header of the snapshot keeps BEAM_ERL_FLAGS of env: a Worker with
|
|
90
|
+
// other flags boots in place of the snapshot. compress gives the pages in
|
|
91
|
+
// gzip (BEAMSNZ1, about a quarter of the size). It is the default at the
|
|
92
|
+
// boot point. A full snapshot has no gzip by default: a stateless Worker
|
|
93
|
+
// restores it in the global scope, which cannot inflate.
|
|
89
94
|
// Gives the bytes of the snapshot.
|
|
90
|
-
export async function snapshot(app, { env = {}, warm = [], kind = 'boot-point' } = {}) {
|
|
95
|
+
export async function snapshot(app, { env = {}, warm = [], kind = 'boot-point', compress = kind !== 'full' } = {}) {
|
|
91
96
|
if (kind !== 'boot-point' && kind !== 'full') throw new Error(`the kind of snapshot is boot-point or full, not ${kind}`);
|
|
92
97
|
const bin = await readApp(app, appFiles);
|
|
93
|
-
const { Vm, releaseMeta } = await import(new URL('worker.js', runtimeUrl).href);
|
|
98
|
+
const { Vm, releaseMeta, packSnapshot } = await import(new URL('worker.js', runtimeUrl).href);
|
|
94
99
|
const meta = releaseMeta(bin);
|
|
95
100
|
if (kind === 'full') {
|
|
96
101
|
if (meta.sql) throw new Error(`${app}: the app has Ecto SQLite: use a snapshot at the boot point`);
|
|
@@ -122,5 +127,5 @@ export async function snapshot(app, { env = {}, warm = [], kind = 'boot-point' }
|
|
|
122
127
|
bytes = await vm.snapshot(false);
|
|
123
128
|
}
|
|
124
129
|
if (!(bytes instanceof Uint8Array)) throw new Error(`${app}: no snapshot (${bytes}): the VM was not quiet`);
|
|
125
|
-
return bytes;
|
|
130
|
+
return compress ? packSnapshot(bytes) : bytes;
|
|
126
131
|
}
|
package/js/wrangler.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The vars of the Wrangler file of a project (wrangler.jsonc, else
|
|
2
|
+
// wrangler.json), for npx beam.com --snapshot: the snapshot gets the
|
|
3
|
+
// BEAM_ERL_FLAGS of the Worker. Wrangler reads JSONC: comments (// and
|
|
4
|
+
// /* */) and a comma before } or ]. wrangler.toml is not read.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
// {file, vars} of the first Wrangler file of dir, or null.
|
|
9
|
+
export function wranglerVars(dir = '.') {
|
|
10
|
+
for (const name of ['wrangler.jsonc', 'wrangler.json']) {
|
|
11
|
+
let text;
|
|
12
|
+
try {
|
|
13
|
+
text = readFileSync(join(dir, name), 'utf8');
|
|
14
|
+
} catch {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const vars = jsonc(text).vars;
|
|
18
|
+
return { file: name, vars: vars && typeof vars === 'object' ? vars : {} };
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The value of a JSONC text.
|
|
24
|
+
export function jsonc(text) {
|
|
25
|
+
let out = '';
|
|
26
|
+
for (let i = 0; i < text.length; i++) {
|
|
27
|
+
const c = text[i];
|
|
28
|
+
if (c === '"') {
|
|
29
|
+
// A string, with its escapes, as it is.
|
|
30
|
+
let j = i + 1;
|
|
31
|
+
while (j < text.length && text[j] !== '"') j += text[j] === '\\' ? 2 : 1;
|
|
32
|
+
out += text.slice(i, j + 1);
|
|
33
|
+
i = j;
|
|
34
|
+
} else if (c === '/' && text[i + 1] === '/') {
|
|
35
|
+
while (i < text.length && text[i] !== '\n') i++;
|
|
36
|
+
out += '\n';
|
|
37
|
+
} else if (c === '/' && text[i + 1] === '*') {
|
|
38
|
+
const end = text.indexOf('*/', i + 2);
|
|
39
|
+
i = end < 0 ? text.length : end + 1;
|
|
40
|
+
out += ' ';
|
|
41
|
+
} else {
|
|
42
|
+
out += c;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// A comma before } or ] (outside the strings, which have no newline).
|
|
46
|
+
return JSON.parse(out.replace(/,(\s*[}\]])/g, '$1'));
|
|
47
|
+
}
|
package/package.json
CHANGED