@swmansion/popcorn 0.4.0-next.4 → 0.4.0-next.6
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 +26 -14
- package/dist/errors.d.ts +2 -1
- package/dist/index.mjs +284 -83
- package/dist/plugins/shared.d.ts +1 -19
- package/dist/plugins/shared.mjs +0 -20
- package/dist/plugins/vite.mjs +6 -1
- package/dist/popcorn.d.ts +56 -43
- package/dist/runtimes/core/beam.wasm +0 -0
- package/dist/runtimes/crypto/beam.wasm +0 -0
- package/dist/types.d.ts +2 -9
- package/dist/worker.mjs +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Run Elixir in a browser with OTP/BEAM compiled to WebAssembly.
|
|
|
4
4
|
This prerelease replaces the AtomVM runtime used by Popcorn 0.3.x.
|
|
5
5
|
|
|
6
6
|
Start with the
|
|
7
|
-
[versioned Popcorn guide](https://popcorn.hexdocs.pm/0.4.0-next.
|
|
7
|
+
[versioned Popcorn guide](https://popcorn.hexdocs.pm/0.4.0-next.6/introduction.html).
|
|
8
8
|
It includes a complete Mix and Vite tutorial.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
@@ -13,12 +13,12 @@ It includes a complete Mix and Vite tutorial.
|
|
|
13
13
|
npm install @swmansion/popcorn@next
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Add `{:popcorn, "0.4.0-next.
|
|
16
|
+
Add `{:popcorn, "0.4.0-next.6"}` to your Elixir application's dependencies.
|
|
17
17
|
Run `mix deps.get` before building the JavaScript application. The bundler
|
|
18
18
|
plugins invoke Mix locally to compile and package application and
|
|
19
19
|
standard-library code.
|
|
20
20
|
|
|
21
|
-
Use the toolchain pinned in [mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.
|
|
21
|
+
Use the toolchain pinned in [mise.toml](https://github.com/software-mansion/popcorn/blob/v0.4.0-next.6/mise.toml).
|
|
22
22
|
The packager checks host OTP compatibility against the selected runtime's manifest.
|
|
23
23
|
|
|
24
24
|
## Configure Vite
|
|
@@ -37,8 +37,20 @@ export default defineConfig({
|
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
Set `rootDir` to the Mix project directory. The current Mix application is the
|
|
40
|
-
default entrypoint.
|
|
41
|
-
|
|
40
|
+
default entrypoint. Configure cooking in the Mix project's `config/config.exs`:
|
|
41
|
+
|
|
42
|
+
```elixir
|
|
43
|
+
import Config
|
|
44
|
+
|
|
45
|
+
config :popcorn,
|
|
46
|
+
extra_apps: [:eex],
|
|
47
|
+
treeshake: [preserved_apps: [:eex]]
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Set `app: :other_app` to select another OTP application, or `app: nil` to
|
|
51
|
+
package the base runtime without starting an application. Cooking settings
|
|
52
|
+
belong exclusively to Elixir config; the JS plugins accept only `rootDir`.
|
|
53
|
+
See `mix help popcorn.cook` for all settings.
|
|
42
54
|
|
|
43
55
|
The npm package contains two variants:
|
|
44
56
|
|
|
@@ -47,8 +59,8 @@ The npm package contains two variants:
|
|
|
47
59
|
|
|
48
60
|
The plugin emits only the selected variant. The browser does not download both.
|
|
49
61
|
Both variants share one JavaScript API and one Hex package.
|
|
50
|
-
The
|
|
51
|
-
Set `
|
|
62
|
+
The cook task selects `crypto` when the application's dependencies or configured `extra_apps` require it; otherwise it selects `core`.
|
|
63
|
+
Set `runtime_variant: :core` or `runtime_variant: :crypto` in Elixir config to override this choice.
|
|
52
64
|
An explicit `"core"` selection produces a build error if the application requires crypto.
|
|
53
65
|
|
|
54
66
|
Rollup and esbuild plugins accept the same options through `@swmansion/popcorn/rollup` and `@swmansion/popcorn/esbuild`.
|
|
@@ -59,18 +71,18 @@ Use ESM output with those bundlers.
|
|
|
59
71
|
```ts
|
|
60
72
|
import { Popcorn } from "@swmansion/popcorn";
|
|
61
73
|
|
|
62
|
-
const
|
|
63
|
-
if (!result.ok) throw result.error;
|
|
64
|
-
|
|
65
|
-
const vm = result.data;
|
|
74
|
+
const vm = await Popcorn.init();
|
|
66
75
|
// Stop the runtime when the application no longer needs it.
|
|
67
76
|
vm.deinit();
|
|
68
77
|
```
|
|
69
78
|
|
|
70
79
|
`Popcorn.init()` waits for the OTP application tree to start. Use
|
|
71
|
-
`popcorn.
|
|
80
|
+
`popcorn.call()` to call a supervised GenServer from JavaScript.
|
|
72
81
|
Use `popcorn.onEvent()` to receive events from BEAM processes.
|
|
73
82
|
|
|
83
|
+
Methods throw or reject on failure. Only `send`, `call`, and `cast` also have
|
|
84
|
+
`trySend`, `tryCall`, and `tryCast` variants that return `Result` objects.
|
|
85
|
+
|
|
74
86
|
Each VM starts with 64 MiB of Wasm linear memory and can grow to 256 MiB on
|
|
75
87
|
phones/tablets or 2 GiB on desktop. Device detection uses the browser's user
|
|
76
88
|
agent and touch support. Configure the limits with
|
|
@@ -91,7 +103,7 @@ Cross-Origin-Embedder-Policy: require-corp
|
|
|
91
103
|
|
|
92
104
|
Vite sets these headers for development and preview. Configure your production server separately.
|
|
93
105
|
Serve `.wasm` as `application/wasm`. Serve compressed `.tar.gz` files with `Content-Encoding: gzip` for `.tar` requests.
|
|
94
|
-
The package also emits uncompressed tar files and Brotli variants. Brotli uses standard effort by default. Set `
|
|
106
|
+
The package also emits uncompressed tar files and Brotli variants. Brotli uses standard effort by default. Set `config :popcorn, brotli_effort: :max` in `config/config.exs` for maximum compression in release builds.
|
|
95
107
|
|
|
96
108
|
The JavaScript bridge currently requires a Content Security Policy that permits `unsafe-eval`.
|
|
97
|
-
See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.
|
|
109
|
+
See the [versioned Elixir API](https://popcorn.hexdocs.pm/0.4.0-next.6/) for interoperability details.
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An operation result. Check `ok` before accessing `data` or `error`.
|
|
3
|
-
* Expected failures use the error branch. Invalid usage can still throw.
|
|
3
|
+
* Expected failures from `try*` methods use the error branch. Invalid usage can still throw.
|
|
4
4
|
*/
|
|
5
5
|
export type Result<T, E extends Tag = Tag> = {
|
|
6
6
|
ok: true;
|
|
@@ -99,4 +99,5 @@ export declare class PopcornError<T extends Tag = Tag> extends Error {
|
|
|
99
99
|
static deserialize(value: unknown): PopcornError;
|
|
100
100
|
}
|
|
101
101
|
export declare function isUnserializableReason(value: unknown): value is UnserializableReason;
|
|
102
|
+
export declare function unwrap<T>(result: Result<T>): T;
|
|
102
103
|
export {};
|
package/dist/index.mjs
CHANGED
|
@@ -172,6 +172,11 @@ function check$1(ok, msg) {
|
|
|
172
172
|
throw err$1("internal:check", { detail: msg });
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
function unwrap(result) {
|
|
176
|
+
if (!result.ok)
|
|
177
|
+
throw result.error;
|
|
178
|
+
return result.data;
|
|
179
|
+
}
|
|
175
180
|
|
|
176
181
|
function base64ToBytes(b64) {
|
|
177
182
|
const binary = atob(b64);
|
|
@@ -191,7 +196,7 @@ function unreachable() {
|
|
|
191
196
|
function objectWithKeys(value, keys) {
|
|
192
197
|
if (value === null || typeof value !== "object")
|
|
193
198
|
return null;
|
|
194
|
-
if (value
|
|
199
|
+
if (Object.getPrototypeOf(value) !== Object.prototype)
|
|
195
200
|
return null;
|
|
196
201
|
const hasAllKeys = keys.every((k) => Object.hasOwn(value, k));
|
|
197
202
|
if (!hasAllKeys)
|
|
@@ -208,7 +213,9 @@ const LARGE_TUPLE_EXT = 0x69;
|
|
|
208
213
|
const NIL_EXT = 0x6a;
|
|
209
214
|
const LIST_EXT = 0x6c;
|
|
210
215
|
const BINARY_EXT = 0x6d;
|
|
216
|
+
const BINARY_HEADER_SIZE = 1 + 4;
|
|
211
217
|
const SMALL_BIG_EXT = 0x6e;
|
|
218
|
+
const LARGE_BIG_EXT = 0x6f;
|
|
212
219
|
const MAP_EXT = 0x74;
|
|
213
220
|
const ATOM_UTF8_EXT = 0x76;
|
|
214
221
|
const SMALL_ATOM_UTF8_EXT = 0x77;
|
|
@@ -309,6 +316,16 @@ class Encoder {
|
|
|
309
316
|
case "number":
|
|
310
317
|
this.number(value);
|
|
311
318
|
return;
|
|
319
|
+
case "bigint":
|
|
320
|
+
this.integer(value);
|
|
321
|
+
return;
|
|
322
|
+
case "symbol": {
|
|
323
|
+
const name = Symbol.keyFor(value) ?? value.description;
|
|
324
|
+
if (name === undefined)
|
|
325
|
+
throw err("unsupported", value);
|
|
326
|
+
this.atom(name);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
312
329
|
case "object":
|
|
313
330
|
this.object(value);
|
|
314
331
|
return;
|
|
@@ -328,29 +345,31 @@ class Encoder {
|
|
|
328
345
|
if (!Number.isSafeInteger(value)) {
|
|
329
346
|
throw err("lossy-int", value);
|
|
330
347
|
}
|
|
331
|
-
|
|
348
|
+
this.integer(BigInt(value));
|
|
349
|
+
}
|
|
350
|
+
integer(value) {
|
|
351
|
+
if (value >= 0n && value < 256n) {
|
|
332
352
|
this.byte(SMALL_INTEGER_EXT);
|
|
333
|
-
this.byte(value);
|
|
353
|
+
this.byte(Number(value));
|
|
334
354
|
return;
|
|
335
355
|
}
|
|
336
|
-
if (value >= -
|
|
356
|
+
if (value >= -2147483648n && value < 2147483648n) {
|
|
337
357
|
this.byte(INTEGER_EXT);
|
|
338
|
-
this.int32(value);
|
|
358
|
+
this.int32(Number(value));
|
|
339
359
|
return;
|
|
340
360
|
}
|
|
341
|
-
|
|
342
|
-
}
|
|
343
|
-
// https://www.erlang.org/doc/apps/erts/erl_ext_dist.html#small_big_ext
|
|
344
|
-
smallBigInt(value) {
|
|
345
|
-
let magnitude = BigInt(Math.abs(value));
|
|
361
|
+
let magnitude = value < 0n ? -value : value;
|
|
346
362
|
const digits = [];
|
|
347
363
|
while (magnitude > 0n) {
|
|
348
364
|
digits.push(Number(magnitude & 0xffn));
|
|
349
365
|
magnitude >>= 8n;
|
|
350
366
|
}
|
|
351
|
-
this.byte(SMALL_BIG_EXT);
|
|
352
|
-
|
|
353
|
-
|
|
367
|
+
this.byte(digits.length < 256 ? SMALL_BIG_EXT : LARGE_BIG_EXT);
|
|
368
|
+
if (digits.length < 256)
|
|
369
|
+
this.byte(digits.length);
|
|
370
|
+
else
|
|
371
|
+
this.uint32(digits.length);
|
|
372
|
+
this.byte(value < 0n ? 1 : 0);
|
|
354
373
|
this.bytes(digits);
|
|
355
374
|
}
|
|
356
375
|
object(rawValue) {
|
|
@@ -376,12 +395,34 @@ class Encoder {
|
|
|
376
395
|
this.array(value);
|
|
377
396
|
return;
|
|
378
397
|
}
|
|
398
|
+
if (value instanceof ArrayBuffer) {
|
|
399
|
+
const bytes = new Uint8Array(value);
|
|
400
|
+
this.binary(bytes);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (ArrayBuffer.isView(value)) {
|
|
404
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
405
|
+
this.binary(bytes);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (value instanceof Date) {
|
|
409
|
+
this.date(value);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (value instanceof Map) {
|
|
413
|
+
this.entries(value.entries(), value);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (value instanceof Set) {
|
|
417
|
+
this.set(value);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
379
420
|
const prototype = Object.getPrototypeOf(value);
|
|
380
421
|
const isObject = prototype === Object.prototype || prototype === null;
|
|
381
422
|
if (!isObject) {
|
|
382
423
|
throw err("non-plain-object", value);
|
|
383
424
|
}
|
|
384
|
-
this.
|
|
425
|
+
this.entries(Object.entries(value), value);
|
|
385
426
|
}
|
|
386
427
|
finally {
|
|
387
428
|
this.ancestors.delete(value);
|
|
@@ -412,18 +453,84 @@ class Encoder {
|
|
|
412
453
|
}
|
|
413
454
|
this.byte(NIL_EXT);
|
|
414
455
|
}
|
|
415
|
-
|
|
416
|
-
|
|
456
|
+
set(value) {
|
|
457
|
+
// Elixir 1.20 uses the version-2 Erlang sets map representation.
|
|
458
|
+
const members = [];
|
|
459
|
+
for (const member of value)
|
|
460
|
+
members.push([member, []]);
|
|
461
|
+
const memberBytes = this.mapBytes(members, value, true);
|
|
462
|
+
const map = new RawTerm(memberBytes);
|
|
463
|
+
const fields = [
|
|
464
|
+
[atom("__struct__"), atom("Elixir.MapSet")],
|
|
465
|
+
[atom("map"), map],
|
|
466
|
+
];
|
|
467
|
+
this.entries(fields, value);
|
|
468
|
+
}
|
|
469
|
+
date(value) {
|
|
470
|
+
const year = value.getUTCFullYear();
|
|
471
|
+
const validDate = Number.isFinite(value.getTime());
|
|
472
|
+
const supportedYear = year >= -9999 && year <= 9999;
|
|
473
|
+
if (!validDate || !supportedYear) {
|
|
474
|
+
throw err("unsupported", value);
|
|
475
|
+
}
|
|
476
|
+
const fields = {
|
|
477
|
+
__struct__: atom("Elixir.DateTime"),
|
|
478
|
+
calendar: atom("Elixir.Calendar.ISO"),
|
|
479
|
+
year,
|
|
480
|
+
month: value.getUTCMonth() + 1,
|
|
481
|
+
day: value.getUTCDate(),
|
|
482
|
+
hour: value.getUTCHours(),
|
|
483
|
+
minute: value.getUTCMinutes(),
|
|
484
|
+
second: value.getUTCSeconds(),
|
|
485
|
+
microsecond: tuple(value.getUTCMilliseconds() * 1000, 3),
|
|
486
|
+
time_zone: "Etc/UTC",
|
|
487
|
+
zone_abbr: "UTC",
|
|
488
|
+
utc_offset: 0,
|
|
489
|
+
std_offset: 0,
|
|
490
|
+
};
|
|
491
|
+
const entries = [];
|
|
492
|
+
for (const [key, item] of Object.entries(fields)) {
|
|
493
|
+
entries.push([atom(key), item]);
|
|
494
|
+
}
|
|
495
|
+
this.entries(entries, value);
|
|
496
|
+
}
|
|
497
|
+
termBytes(value) {
|
|
498
|
+
const start = this.output.length;
|
|
499
|
+
this.value(value);
|
|
500
|
+
return this.output.splice(start);
|
|
501
|
+
}
|
|
502
|
+
mapBytes(entries, source, merge = false) {
|
|
503
|
+
const encoded = [...entries].map(([key, value]) => ({
|
|
504
|
+
key: this.termBytes(key),
|
|
505
|
+
value: this.termBytes(value),
|
|
506
|
+
}));
|
|
507
|
+
encoded.sort((left, right) => compareKeys(left.key, right.key));
|
|
508
|
+
const unique = [];
|
|
509
|
+
for (const entry of encoded) {
|
|
510
|
+
const previous = unique.at(-1);
|
|
511
|
+
const duplicate = previous !== undefined && compareBytes(entry.key, previous.key) === 0;
|
|
512
|
+
if (duplicate) {
|
|
513
|
+
if (!merge)
|
|
514
|
+
throw err("unsupported", source);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
unique.push(entry);
|
|
518
|
+
}
|
|
519
|
+
const start = this.output.length;
|
|
417
520
|
this.byte(MAP_EXT);
|
|
418
|
-
this.uint32(
|
|
419
|
-
for (const
|
|
420
|
-
this.
|
|
421
|
-
this.
|
|
521
|
+
this.uint32(unique.length);
|
|
522
|
+
for (const entry of unique) {
|
|
523
|
+
this.bytes(entry.key);
|
|
524
|
+
this.bytes(entry.value);
|
|
422
525
|
}
|
|
526
|
+
return new Uint8Array(this.output.splice(start));
|
|
527
|
+
}
|
|
528
|
+
entries(entries, source) {
|
|
529
|
+
this.bytes(this.mapBytes(entries, source));
|
|
423
530
|
}
|
|
424
531
|
atom(atom) {
|
|
425
532
|
const bytes = UTF8$1.encode(atom);
|
|
426
|
-
if (
|
|
533
|
+
if ([...atom].length > 255) {
|
|
427
534
|
throw err("unsupported", atom);
|
|
428
535
|
}
|
|
429
536
|
if (bytes.length < 2 ** 8) {
|
|
@@ -437,7 +544,7 @@ class Encoder {
|
|
|
437
544
|
this.bytes(bytes);
|
|
438
545
|
}
|
|
439
546
|
binary(value) {
|
|
440
|
-
const bytes = UTF8$1.encode(value);
|
|
547
|
+
const bytes = typeof value === "string" ? UTF8$1.encode(value) : value;
|
|
441
548
|
this.byte(BINARY_EXT);
|
|
442
549
|
this.uint32(bytes.length);
|
|
443
550
|
this.bytes(bytes);
|
|
@@ -470,6 +577,21 @@ class Encoder {
|
|
|
470
577
|
function err(reason, part) {
|
|
471
578
|
return new TypeError(reason, { cause: part });
|
|
472
579
|
}
|
|
580
|
+
function compareBytes(a, b, start = 0) {
|
|
581
|
+
const length = Math.min(a.length, b.length);
|
|
582
|
+
for (let i = start; i < length; i++) {
|
|
583
|
+
if (a[i] !== b[i])
|
|
584
|
+
return a[i] - b[i];
|
|
585
|
+
}
|
|
586
|
+
return a.length - b.length;
|
|
587
|
+
}
|
|
588
|
+
function compareKeys(left, right) {
|
|
589
|
+
const leftTag = left[0];
|
|
590
|
+
const rightTag = right[0];
|
|
591
|
+
const binaryKeys = leftTag === BINARY_EXT && rightTag === BINARY_EXT;
|
|
592
|
+
const offset = binaryKeys ? BINARY_HEADER_SIZE : 0;
|
|
593
|
+
return compareBytes(left, right, offset);
|
|
594
|
+
}
|
|
473
595
|
|
|
474
596
|
function readWorkerEvent(value) {
|
|
475
597
|
const data = objectWithKeys(value, ["type", "payload"]);
|
|
@@ -544,6 +666,10 @@ function assertRunJsFn(value) {
|
|
|
544
666
|
* Use {@link Popcorn.init} to create and start an instance.
|
|
545
667
|
**/
|
|
546
668
|
class Popcorn {
|
|
669
|
+
/** Receives stdout. Can be replaced at any time; `undefined` restores the default output handler. */
|
|
670
|
+
onStdout;
|
|
671
|
+
/** Receives stderr. Can be replaced at any time; `undefined` restores the default output handler. */
|
|
672
|
+
onStderr;
|
|
547
673
|
vmWorker;
|
|
548
674
|
state = { status: "created" };
|
|
549
675
|
opts;
|
|
@@ -561,10 +687,14 @@ class Popcorn {
|
|
|
561
687
|
trackedKeySeq = 0;
|
|
562
688
|
io = createIoState();
|
|
563
689
|
vmReady = false;
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
690
|
+
/** Calls a GenServer and returns its reply. See {@link tryCall} for a result instead of throwing. */
|
|
691
|
+
async call(target, request, opts) {
|
|
692
|
+
return unwrap(await this.tryCall(target, request, opts));
|
|
693
|
+
}
|
|
694
|
+
/** Sends a cast through the proxy. See {@link tryCast} for a result instead of throwing. */
|
|
695
|
+
async cast(target, request, opts) {
|
|
696
|
+
unwrap(await this.tryCast(target, request, opts));
|
|
697
|
+
}
|
|
568
698
|
TrackedValue = class {
|
|
569
699
|
value;
|
|
570
700
|
cleanup;
|
|
@@ -635,7 +765,11 @@ class Popcorn {
|
|
|
635
765
|
},
|
|
636
766
|
};
|
|
637
767
|
this.ttySize = { ...ttySize };
|
|
638
|
-
this.
|
|
768
|
+
this.onStdout = opts.onStdout;
|
|
769
|
+
this.onStderr = opts.onStderr;
|
|
770
|
+
this.output = resolveOutputHandlers(this.opts, this);
|
|
771
|
+
if (opts.onEvent)
|
|
772
|
+
this.onEvent(opts.onEvent);
|
|
639
773
|
this.spawnWorker();
|
|
640
774
|
}
|
|
641
775
|
spawnWorker() {
|
|
@@ -650,20 +784,18 @@ class Popcorn {
|
|
|
650
784
|
/**
|
|
651
785
|
* Creates an instance and waits for {@link boot}.
|
|
652
786
|
*
|
|
653
|
-
*
|
|
787
|
+
* Use `opts.onEvent` to receive messages sent during startup.
|
|
788
|
+
*
|
|
789
|
+
* @returns The instance. Rejects on failure.
|
|
654
790
|
*
|
|
655
|
-
* @
|
|
791
|
+
* @see {@link PopcornOpts}
|
|
656
792
|
*/
|
|
657
793
|
static async init(opts = {}) {
|
|
658
794
|
if (!canEval()) {
|
|
659
|
-
|
|
795
|
+
throw err$1("runtime:eval-unavailable", {});
|
|
660
796
|
}
|
|
661
797
|
const popcorn = new Popcorn(opts);
|
|
662
|
-
|
|
663
|
-
if (!result.ok) {
|
|
664
|
-
return result;
|
|
665
|
-
}
|
|
666
|
-
return { ok: true, data: popcorn };
|
|
798
|
+
return await popcorn.boot();
|
|
667
799
|
}
|
|
668
800
|
/**
|
|
669
801
|
* Starts the VM and waits for its bridge and entrypoint application.
|
|
@@ -672,37 +804,29 @@ class Popcorn {
|
|
|
672
804
|
*
|
|
673
805
|
* After shutdown, starts a fresh VM with the original options.
|
|
674
806
|
*
|
|
675
|
-
* @returns
|
|
807
|
+
* @returns This instance. Rejects on failure.
|
|
676
808
|
*
|
|
677
809
|
* @example
|
|
678
810
|
* ```ts
|
|
679
811
|
* const popcorn = new Popcorn({});
|
|
680
812
|
* popcorn.onEvent((message) => console.log(message));
|
|
681
|
-
*
|
|
682
|
-
* if (!result.ok) throw result.error;
|
|
813
|
+
* await popcorn.boot();
|
|
683
814
|
* ```
|
|
684
815
|
*/
|
|
685
816
|
async boot() {
|
|
686
|
-
if (this.state.status === "booted")
|
|
687
|
-
return
|
|
688
|
-
|
|
689
|
-
if (this.state.status === "booting") {
|
|
690
|
-
// TODO(jgonet): make it easier to construct check() errors without throwing
|
|
691
|
-
const error = err$1("internal:check", {
|
|
692
|
-
detail: "Boot already in progress",
|
|
693
|
-
});
|
|
694
|
-
return { ok: false, error };
|
|
695
|
-
}
|
|
817
|
+
if (this.state.status === "booted")
|
|
818
|
+
return this;
|
|
819
|
+
check(this.state.status !== "booting", "Boot already in progress");
|
|
696
820
|
const reboot = this.state.status === "closed";
|
|
697
821
|
if (reboot) {
|
|
698
822
|
this.spawnWorker();
|
|
699
823
|
}
|
|
700
824
|
this.Pid = createPidClass();
|
|
701
825
|
this.io = createIoState();
|
|
702
|
-
this.output = resolveOutputHandlers(this.opts);
|
|
826
|
+
this.output = resolveOutputHandlers(this.opts, this);
|
|
703
827
|
check(this.readyWaiters.length === 0);
|
|
704
828
|
this.state = { status: "booting" };
|
|
705
|
-
return await new Promise((resolve) => {
|
|
829
|
+
return await new Promise((resolve, reject) => {
|
|
706
830
|
const timeoutsMs = { ...DEFAULT_TIMEOUTS_MS, ...this.opts.timeoutsMs };
|
|
707
831
|
const settle = (result) => {
|
|
708
832
|
if (this.settleBoot === null)
|
|
@@ -712,7 +836,10 @@ class Popcorn {
|
|
|
712
836
|
if (!result.ok) {
|
|
713
837
|
this.deinit();
|
|
714
838
|
}
|
|
715
|
-
|
|
839
|
+
if (result.ok)
|
|
840
|
+
resolve(result.data);
|
|
841
|
+
else
|
|
842
|
+
reject(result.error);
|
|
716
843
|
};
|
|
717
844
|
this.settleBoot = settle;
|
|
718
845
|
const startPhase = (timeoutMs) => setTimeout(() => {
|
|
@@ -763,13 +890,12 @@ class Popcorn {
|
|
|
763
890
|
*
|
|
764
891
|
* Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
|
|
765
892
|
*
|
|
766
|
-
*
|
|
893
|
+
* Throws `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
|
|
767
894
|
* An overflow leaves the queue unchanged.
|
|
768
895
|
*/
|
|
769
896
|
writeStdin(chunk) {
|
|
770
|
-
if (this.state.status === "closed")
|
|
771
|
-
|
|
772
|
-
}
|
|
897
|
+
if (this.state.status === "closed")
|
|
898
|
+
throw this.state.error;
|
|
773
899
|
check(this.state.status === "booted");
|
|
774
900
|
const bytes = toBytes(chunk);
|
|
775
901
|
check(bytes.byteLength > 0);
|
|
@@ -779,29 +905,28 @@ class Popcorn {
|
|
|
779
905
|
capacityBytes: STDIN_QUEUE_CAPACITY_BYTES,
|
|
780
906
|
attemptedBytes,
|
|
781
907
|
});
|
|
782
|
-
|
|
908
|
+
throw error;
|
|
783
909
|
}
|
|
784
910
|
this.io.stdin.reservedBytes = attemptedBytes;
|
|
785
911
|
const event = { type: "popcorn:stdin", payload: { chunk: bytes } };
|
|
786
912
|
toVm(this.vmWorker, event, [bytes.buffer]);
|
|
787
|
-
return { ok: true, data: null };
|
|
788
913
|
}
|
|
789
914
|
/**
|
|
790
915
|
* Sends new terminal dimensions to a booted VM.
|
|
791
916
|
*
|
|
792
917
|
* Each dimension must be between 1 and 65,535.
|
|
918
|
+
*
|
|
919
|
+
* Throws on failure.
|
|
793
920
|
*/
|
|
794
921
|
resizeTty(columns, rows) {
|
|
795
|
-
if (this.state.status === "closed")
|
|
796
|
-
|
|
797
|
-
}
|
|
922
|
+
if (this.state.status === "closed")
|
|
923
|
+
throw this.state.error;
|
|
798
924
|
check(this.state.status === "booted");
|
|
799
925
|
check(isValidTtySize({ columns, rows }));
|
|
800
926
|
toVm(this.vmWorker, {
|
|
801
927
|
type: "popcorn:tty-resize",
|
|
802
928
|
payload: { columns, rows },
|
|
803
929
|
});
|
|
804
|
-
return { ok: true, data: null };
|
|
805
930
|
}
|
|
806
931
|
/**
|
|
807
932
|
* Sends a payload to a registered process name or a {@link Pid} from this VM boot.
|
|
@@ -810,21 +935,26 @@ class Popcorn {
|
|
|
810
935
|
* A send timeout does not cancel delivery.
|
|
811
936
|
* Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
|
|
812
937
|
*
|
|
813
|
-
* @returns
|
|
938
|
+
* @returns Rejects on failure.
|
|
814
939
|
*
|
|
815
940
|
* @example
|
|
816
941
|
* Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
|
|
817
942
|
*
|
|
818
943
|
* ```ts
|
|
819
|
-
*
|
|
820
|
-
* if (!result.ok) throw result.error;
|
|
944
|
+
* await popcorn.send("receiver", tuple(atom("ok"), "value"));
|
|
821
945
|
* ```
|
|
822
946
|
*
|
|
823
947
|
* @see {@link AnyValue}
|
|
824
948
|
* @see {@link atom}
|
|
825
949
|
* @see {@link tuple}
|
|
950
|
+
*
|
|
951
|
+
* @see {@link trySend} for a result instead of throwing on expected failures.
|
|
826
952
|
*/
|
|
827
953
|
async send(rawTarget, payload) {
|
|
954
|
+
unwrap(await this.trySend(rawTarget, payload));
|
|
955
|
+
}
|
|
956
|
+
/** See {@link send}. */
|
|
957
|
+
async trySend(rawTarget, payload) {
|
|
828
958
|
if (this.state.status !== "booted") {
|
|
829
959
|
if (this.state.status === "closed") {
|
|
830
960
|
return { ok: false, error: this.state.error };
|
|
@@ -978,7 +1108,16 @@ class Popcorn {
|
|
|
978
1108
|
unreachable();
|
|
979
1109
|
}
|
|
980
1110
|
}
|
|
981
|
-
|
|
1111
|
+
/**
|
|
1112
|
+
* Calls a GenServer by registered name or {@link Pid} and waits for its reply.
|
|
1113
|
+
*
|
|
1114
|
+
* @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy (see `Popcorn.Proxy`).
|
|
1115
|
+
* @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
|
|
1116
|
+
*
|
|
1117
|
+
* GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
|
|
1118
|
+
* A call timeout does not cancel server work.
|
|
1119
|
+
*/
|
|
1120
|
+
async tryCall(rawTarget, request, opts) {
|
|
982
1121
|
if (this.state.status !== "booted") {
|
|
983
1122
|
if (this.state.status === "closed") {
|
|
984
1123
|
return { ok: false, error: this.state.error };
|
|
@@ -1021,7 +1160,14 @@ class Popcorn {
|
|
|
1021
1160
|
}
|
|
1022
1161
|
return result;
|
|
1023
1162
|
}
|
|
1024
|
-
|
|
1163
|
+
/**
|
|
1164
|
+
* Sends a cast through the proxy.
|
|
1165
|
+
*
|
|
1166
|
+
* Success confirms delivery to the proxy
|
|
1167
|
+
* @see {@link tryCall}
|
|
1168
|
+
* @param opts - `proxy` allows to select another registered proxy.
|
|
1169
|
+
*/
|
|
1170
|
+
async tryCast(rawTarget, request, opts) {
|
|
1025
1171
|
if (this.state.status !== "booted") {
|
|
1026
1172
|
if (this.state.status === "closed") {
|
|
1027
1173
|
return { ok: false, error: this.state.error };
|
|
@@ -1050,9 +1196,16 @@ class Popcorn {
|
|
|
1050
1196
|
const args = this.reviveHandles(request.args);
|
|
1051
1197
|
check(this.vmReady);
|
|
1052
1198
|
const actions = {
|
|
1053
|
-
send: (target, payload) =>
|
|
1054
|
-
|
|
1055
|
-
|
|
1199
|
+
send: async (target, payload) => {
|
|
1200
|
+
unwrap(await this.sendBridge(target, payload));
|
|
1201
|
+
},
|
|
1202
|
+
call: async (target, payload, opts) => unwrap(await this.callBridge(target, payload, opts)),
|
|
1203
|
+
cast: async (target, payload, opts) => {
|
|
1204
|
+
unwrap(await this.castBridge(target, payload, opts));
|
|
1205
|
+
},
|
|
1206
|
+
trySend: (target, payload) => this.sendBridge(target, payload),
|
|
1207
|
+
tryCall: (target, payload, opts) => this.callBridge(target, payload, opts),
|
|
1208
|
+
tryCast: (target, payload, opts) => this.castBridge(target, payload, opts),
|
|
1056
1209
|
};
|
|
1057
1210
|
const result = await fn(args, actions);
|
|
1058
1211
|
const value = request.return === "ref" ? this.asRef(result) : result;
|
|
@@ -1110,6 +1263,10 @@ class Popcorn {
|
|
|
1110
1263
|
return make(this.TrackedValue);
|
|
1111
1264
|
}
|
|
1112
1265
|
reviveHandles(value) {
|
|
1266
|
+
const marker = objectWithKeys(value, ["popcorn_value"]);
|
|
1267
|
+
if (marker !== null && Object.keys(marker).length === 1) {
|
|
1268
|
+
return this.reviveValueTag(marker.popcorn_value);
|
|
1269
|
+
}
|
|
1113
1270
|
const key = trackedRefKey(value);
|
|
1114
1271
|
if (key !== null) {
|
|
1115
1272
|
const entry = this.trackedValues.get(key);
|
|
@@ -1125,14 +1282,55 @@ class Popcorn {
|
|
|
1125
1282
|
}
|
|
1126
1283
|
const obj = objectWithKeys(value, []);
|
|
1127
1284
|
if (obj !== null) {
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
return
|
|
1285
|
+
const entries = Object.entries(obj).map(([key, item]) => [
|
|
1286
|
+
key,
|
|
1287
|
+
this.reviveHandles(item),
|
|
1288
|
+
]);
|
|
1289
|
+
return Object.fromEntries(entries);
|
|
1133
1290
|
}
|
|
1134
1291
|
return value;
|
|
1135
1292
|
}
|
|
1293
|
+
reviveValueTag(payload) {
|
|
1294
|
+
check(Array.isArray(payload) && payload.length === 2);
|
|
1295
|
+
const [tag, data] = payload;
|
|
1296
|
+
switch (tag) {
|
|
1297
|
+
case "binary":
|
|
1298
|
+
check(typeof data === "string");
|
|
1299
|
+
return base64ToBytes(data);
|
|
1300
|
+
case "integer": {
|
|
1301
|
+
const integer = readInteger(data);
|
|
1302
|
+
const safe = integer >= BigInt(Number.MIN_SAFE_INTEGER) &&
|
|
1303
|
+
integer <= BigInt(Number.MAX_SAFE_INTEGER);
|
|
1304
|
+
if (safe)
|
|
1305
|
+
return Number(integer);
|
|
1306
|
+
return integer;
|
|
1307
|
+
}
|
|
1308
|
+
case "datetime": {
|
|
1309
|
+
const milliseconds = readInteger(data);
|
|
1310
|
+
const inRange = milliseconds >= -377705116800000n && milliseconds <= 253402300799999n;
|
|
1311
|
+
check(inRange);
|
|
1312
|
+
return new Date(Number(milliseconds));
|
|
1313
|
+
}
|
|
1314
|
+
case "set":
|
|
1315
|
+
check(Array.isArray(data));
|
|
1316
|
+
return new Set(data.map((item) => this.reviveHandles(item)));
|
|
1317
|
+
case "object": {
|
|
1318
|
+
check(Array.isArray(data));
|
|
1319
|
+
const entries = [];
|
|
1320
|
+
for (const entry of data) {
|
|
1321
|
+
check(Array.isArray(entry) && entry.length === 2);
|
|
1322
|
+
const [key, value] = entry;
|
|
1323
|
+
check(typeof key === "string");
|
|
1324
|
+
entries.push([key, this.reviveHandles(value)]);
|
|
1325
|
+
}
|
|
1326
|
+
const object = Object.fromEntries(entries);
|
|
1327
|
+
check(Object.keys(object).length === entries.length);
|
|
1328
|
+
return object;
|
|
1329
|
+
}
|
|
1330
|
+
default:
|
|
1331
|
+
check(false, "unknown popcorn value tag");
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1136
1334
|
/** Maps pids and `TrackedValue`s during encoding, collecting handles into
|
|
1137
1335
|
* `tracked` for the caller to register once encoding succeeds. */
|
|
1138
1336
|
handleMapper(tracked) {
|
|
@@ -1228,22 +1426,20 @@ function isValidTtySize({ columns, rows }) {
|
|
|
1228
1426
|
const rowInRange = 0 < rows && rows <= 0xffff;
|
|
1229
1427
|
return colInRange && rowInRange;
|
|
1230
1428
|
}
|
|
1231
|
-
function resolveOutputHandlers(opts) {
|
|
1429
|
+
function resolveOutputHandlers(opts, callbacks) {
|
|
1232
1430
|
if (opts.tty?.output === "bytes") {
|
|
1233
|
-
const onStdout = opts.onStdout;
|
|
1234
|
-
const onStderr = opts.onStderr;
|
|
1235
1431
|
return {
|
|
1236
|
-
stdout: onStdout ??
|
|
1237
|
-
|
|
1432
|
+
stdout: (chunk) => (callbacks.onStdout ??
|
|
1433
|
+
defaultOnStdoutBytes)(chunk),
|
|
1434
|
+
stderr: (chunk) => (callbacks.onStderr ??
|
|
1435
|
+
defaultOnStderrBytes)(chunk),
|
|
1238
1436
|
};
|
|
1239
1437
|
}
|
|
1240
1438
|
const stdoutDecoder = new TextDecoder();
|
|
1241
1439
|
const stderrDecoder = new TextDecoder();
|
|
1242
|
-
const onStdout = opts.onStdout ?? defaultOnStdout;
|
|
1243
|
-
const onStderr = opts.onStderr ?? defaultOnStderr;
|
|
1244
1440
|
return {
|
|
1245
|
-
stdout: (chunk) => decodeOutput(stdoutDecoder, onStdout, chunk),
|
|
1246
|
-
stderr: (chunk) => decodeOutput(stderrDecoder, onStderr, chunk),
|
|
1441
|
+
stdout: (chunk) => decodeOutput(stdoutDecoder, callbacks.onStdout ?? defaultOnStdout, chunk),
|
|
1442
|
+
stderr: (chunk) => decodeOutput(stderrDecoder, callbacks.onStderr ?? defaultOnStderr, chunk),
|
|
1247
1443
|
};
|
|
1248
1444
|
}
|
|
1249
1445
|
function decodeOutput(decoder, onOutput, chunk) {
|
|
@@ -1333,5 +1529,10 @@ function defaultOnError(payload) {
|
|
|
1333
1529
|
unreachable();
|
|
1334
1530
|
}
|
|
1335
1531
|
}
|
|
1532
|
+
function readInteger(value) {
|
|
1533
|
+
check(typeof value === "string");
|
|
1534
|
+
check(/^-?(0|[1-9][0-9]*)$/.test(value));
|
|
1535
|
+
return BigInt(value);
|
|
1536
|
+
}
|
|
1336
1537
|
|
|
1337
1538
|
export { Popcorn, PopcornError, a, atom, schedulers, t, tuple };
|
package/dist/plugins/shared.d.ts
CHANGED
|
@@ -6,26 +6,8 @@ type RuntimeVariant = "core" | "crypto";
|
|
|
6
6
|
* compiles the project before packaging it.
|
|
7
7
|
*/
|
|
8
8
|
export type Options = {
|
|
9
|
-
/**
|
|
10
|
-
runtimeVariant?: RuntimeVariant;
|
|
11
|
-
/** Mix project directory. */
|
|
9
|
+
/** Mix project directory. Cooking options belong in its config/config.exs. */
|
|
12
10
|
rootDir: string;
|
|
13
|
-
/**
|
|
14
|
-
* OTP application to start after VM boot.
|
|
15
|
-
* Defaults to the current Mix application.
|
|
16
|
-
*/
|
|
17
|
-
app?: string | null;
|
|
18
|
-
/** Additional applications to package with their dependencies. */
|
|
19
|
-
extraApps?: string[];
|
|
20
|
-
/** Brotli compression effort. Defaults to `standard`. */
|
|
21
|
-
brotliEffort?: "standard" | "max";
|
|
22
|
-
/** Removes nonessential BEAM chunks. Defaults to `true`. */
|
|
23
|
-
strip?: boolean;
|
|
24
|
-
/** Controls removal of unreachable modules and functions. Defaults to `none`. */
|
|
25
|
-
treeshake?: TreeshakeOptions;
|
|
26
|
-
};
|
|
27
|
-
export type TreeshakeOptions = {
|
|
28
|
-
preservedApps?: string[];
|
|
29
11
|
};
|
|
30
12
|
export type Prepared = {
|
|
31
13
|
dir: string;
|
package/dist/plugins/shared.mjs
CHANGED
|
@@ -9,26 +9,6 @@ const execFileAsync = promisify(execFile);
|
|
|
9
9
|
async function popcorn(options) {
|
|
10
10
|
const dir = await mkdtemp(`${tmpdir()}/popcorn-otp-`);
|
|
11
11
|
const args = ["popcorn.cook", "--out-dir", dir];
|
|
12
|
-
if (options.app === null)
|
|
13
|
-
args.push("--no-app");
|
|
14
|
-
else if (options.app !== undefined)
|
|
15
|
-
args.push("--app", options.app);
|
|
16
|
-
for (const app of options.extraApps ?? [])
|
|
17
|
-
args.push("--extra-app", app);
|
|
18
|
-
if (options.runtimeVariant !== undefined) {
|
|
19
|
-
args.push("--runtime-variant", options.runtimeVariant);
|
|
20
|
-
}
|
|
21
|
-
if (options.brotliEffort !== undefined) {
|
|
22
|
-
args.push("--brotli-effort", options.brotliEffort);
|
|
23
|
-
}
|
|
24
|
-
if (options.strip === false)
|
|
25
|
-
args.push("--no-strip");
|
|
26
|
-
if (options.treeshake !== undefined) {
|
|
27
|
-
args.push("--treeshake");
|
|
28
|
-
for (const app of options.treeshake.preservedApps ?? []) {
|
|
29
|
-
args.push("--preserved-app", app);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
12
|
try {
|
|
33
13
|
await execFileAsync("mix", args, {
|
|
34
14
|
cwd: resolve(options.rootDir),
|
package/dist/plugins/vite.mjs
CHANGED
|
@@ -43,6 +43,7 @@ function popcorn(options) {
|
|
|
43
43
|
let dirty = false;
|
|
44
44
|
let outDir;
|
|
45
45
|
let assetsDir = "assets";
|
|
46
|
+
let workerDerivedOtpRoot = `/@fs${distDir}/otp/`;
|
|
46
47
|
// Dev/preview: pack once at server startup, then re-pack lazily after the
|
|
47
48
|
// app's compiled beams change. A single in-flight promise dedupes concurrent
|
|
48
49
|
// requests; a change during a re-pack re-sets `dirty` so it isn't lost.
|
|
@@ -78,7 +79,7 @@ function popcorn(options) {
|
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
80
81
|
const requestPath = decodeURIComponent(new URL(url, "http://localhost").pathname);
|
|
81
|
-
const requestRoot = [`/${assetsDir}/otp/`,
|
|
82
|
+
const requestRoot = [`/${assetsDir}/otp/`, workerDerivedOtpRoot].find((root) => requestPath.startsWith(root));
|
|
82
83
|
if (requestRoot === undefined) {
|
|
83
84
|
next();
|
|
84
85
|
return;
|
|
@@ -146,6 +147,10 @@ function popcorn(options) {
|
|
|
146
147
|
config.server.fs.allow.push(distDir);
|
|
147
148
|
outDir = resolve(config.root, config.build.outDir);
|
|
148
149
|
assetsDir = config.build.assetsDir;
|
|
150
|
+
const servedDistDir = isUnder(config.root, distDir)
|
|
151
|
+
? `/${relative(config.root, distDir)}`
|
|
152
|
+
: `/@fs${distDir}`;
|
|
153
|
+
workerDerivedOtpRoot = `${servedDistDir}/otp/`;
|
|
149
154
|
},
|
|
150
155
|
async configureServer(server) {
|
|
151
156
|
const rootDir = resolve(options.rootDir);
|
package/dist/popcorn.d.ts
CHANGED
|
@@ -54,14 +54,14 @@ export type PopcornOpts<Output extends TtyOutput = "text"> = {
|
|
|
54
54
|
* Receives stdout.
|
|
55
55
|
*
|
|
56
56
|
* Defaults to `console.log`.
|
|
57
|
-
*
|
|
57
|
+
* Receives `Uint8Array` when `tty.output` is "bytes", and `string` otherwise.
|
|
58
58
|
*/
|
|
59
59
|
onStdout?: (chunk: OutputChunk<Output>) => void;
|
|
60
60
|
/**
|
|
61
61
|
* Receives stderr.
|
|
62
62
|
*
|
|
63
63
|
* Defaults to `console.error`.
|
|
64
|
-
*
|
|
64
|
+
* Receives `Uint8Array` when `tty.output` is "bytes", and `string` otherwise.
|
|
65
65
|
*/
|
|
66
66
|
onStderr?: (chunk: OutputChunk<Output>) => void;
|
|
67
67
|
/**
|
|
@@ -70,6 +70,8 @@ export type PopcornOpts<Output extends TtyOutput = "text"> = {
|
|
|
70
70
|
* Defaults to console output.
|
|
71
71
|
*/
|
|
72
72
|
onError?: (event: OtpErrorPayload) => void;
|
|
73
|
+
/** Receives BEAM message payloads, including messages sent during startup. */
|
|
74
|
+
onEvent?: (event: PopcornEvent) => void;
|
|
73
75
|
/**
|
|
74
76
|
* Module worker URL.
|
|
75
77
|
*
|
|
@@ -93,36 +95,17 @@ type CallOpts = {
|
|
|
93
95
|
timeoutMs?: number;
|
|
94
96
|
proxy?: string;
|
|
95
97
|
};
|
|
96
|
-
export type GenServer =
|
|
97
|
-
/**
|
|
98
|
-
* Calls a GenServer by registered name or {@link Pid} and waits for its reply.
|
|
99
|
-
*
|
|
100
|
-
* Requires a running `Popcorn.Proxy`, registered as `popcorn_proxy` by default.
|
|
101
|
-
*
|
|
102
|
-
* @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy.
|
|
103
|
-
* @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
|
|
104
|
-
*
|
|
105
|
-
* GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
|
|
106
|
-
* A call timeout does not cancel server work.
|
|
107
|
-
*/
|
|
108
|
-
call(target: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<Result<AnyValue>>;
|
|
109
|
-
/**
|
|
110
|
-
* Sends a cast through the proxy.
|
|
111
|
-
*
|
|
112
|
-
* Success confirms delivery to the proxy
|
|
113
|
-
* @see {@link call}
|
|
114
|
-
* @param opts - `proxy` allows to select another registered proxy.
|
|
115
|
-
*/
|
|
116
|
-
cast(target: string | Pid, request?: AnyValue, opts?: {
|
|
117
|
-
proxy?: string;
|
|
118
|
-
}): Promise<Result<null>>;
|
|
119
|
-
};
|
|
98
|
+
export type GenServer = Pick<Popcorn, "call" | "cast" | "tryCall" | "tryCast">;
|
|
120
99
|
/**
|
|
121
100
|
* A BEAM VM in a browser worker.
|
|
122
101
|
*
|
|
123
102
|
* Use {@link Popcorn.init} to create and start an instance.
|
|
124
103
|
**/
|
|
125
104
|
export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
105
|
+
/** Receives stdout. Can be replaced at any time; `undefined` restores the default output handler. */
|
|
106
|
+
onStdout?: (chunk: OutputChunk<Output>) => void;
|
|
107
|
+
/** Receives stderr. Can be replaced at any time; `undefined` restores the default output handler. */
|
|
108
|
+
onStderr?: (chunk: OutputChunk<Output>) => void;
|
|
126
109
|
private vmWorker;
|
|
127
110
|
private state;
|
|
128
111
|
private readonly opts;
|
|
@@ -140,7 +123,12 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
140
123
|
private trackedKeySeq;
|
|
141
124
|
private io;
|
|
142
125
|
private vmReady;
|
|
143
|
-
|
|
126
|
+
/** Calls a GenServer and returns its reply. See {@link tryCall} for a result instead of throwing. */
|
|
127
|
+
call(target: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<AnyValue>;
|
|
128
|
+
/** Sends a cast through the proxy. See {@link tryCast} for a result instead of throwing. */
|
|
129
|
+
cast(target: string | Pid, request?: AnyValue, opts?: {
|
|
130
|
+
proxy?: string;
|
|
131
|
+
}): Promise<void>;
|
|
144
132
|
private readonly TrackedValue;
|
|
145
133
|
private Pid;
|
|
146
134
|
private readonly onWorkerMessage;
|
|
@@ -154,11 +142,13 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
154
142
|
/**
|
|
155
143
|
* Creates an instance and waits for {@link boot}.
|
|
156
144
|
*
|
|
157
|
-
*
|
|
145
|
+
* Use `opts.onEvent` to receive messages sent during startup.
|
|
146
|
+
*
|
|
147
|
+
* @returns The instance. Rejects on failure.
|
|
158
148
|
*
|
|
159
|
-
* @
|
|
149
|
+
* @see {@link PopcornOpts}
|
|
160
150
|
*/
|
|
161
|
-
static init<Output extends TtyOutput = "text">(opts?: PopcornOpts<Output>): Promise<
|
|
151
|
+
static init<Output extends TtyOutput = "text">(opts?: PopcornOpts<Output>): Promise<Popcorn<Output>>;
|
|
162
152
|
/**
|
|
163
153
|
* Starts the VM and waits for its bridge and entrypoint application.
|
|
164
154
|
*
|
|
@@ -166,32 +156,33 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
166
156
|
*
|
|
167
157
|
* After shutdown, starts a fresh VM with the original options.
|
|
168
158
|
*
|
|
169
|
-
* @returns
|
|
159
|
+
* @returns This instance. Rejects on failure.
|
|
170
160
|
*
|
|
171
161
|
* @example
|
|
172
162
|
* ```ts
|
|
173
163
|
* const popcorn = new Popcorn({});
|
|
174
164
|
* popcorn.onEvent((message) => console.log(message));
|
|
175
|
-
*
|
|
176
|
-
* if (!result.ok) throw result.error;
|
|
165
|
+
* await popcorn.boot();
|
|
177
166
|
* ```
|
|
178
167
|
*/
|
|
179
|
-
boot(): Promise<
|
|
168
|
+
boot(): Promise<Popcorn<Output>>;
|
|
180
169
|
/**
|
|
181
170
|
* Queues terminal input.
|
|
182
171
|
*
|
|
183
172
|
* Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
|
|
184
173
|
*
|
|
185
|
-
*
|
|
174
|
+
* Throws `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
|
|
186
175
|
* An overflow leaves the queue unchanged.
|
|
187
176
|
*/
|
|
188
|
-
writeStdin(chunk: string | Uint8Array):
|
|
177
|
+
writeStdin(chunk: string | Uint8Array): void;
|
|
189
178
|
/**
|
|
190
179
|
* Sends new terminal dimensions to a booted VM.
|
|
191
180
|
*
|
|
192
181
|
* Each dimension must be between 1 and 65,535.
|
|
182
|
+
*
|
|
183
|
+
* Throws on failure.
|
|
193
184
|
*/
|
|
194
|
-
resizeTty(columns: number, rows: number):
|
|
185
|
+
resizeTty(columns: number, rows: number): void;
|
|
195
186
|
/**
|
|
196
187
|
* Sends a payload to a registered process name or a {@link Pid} from this VM boot.
|
|
197
188
|
*
|
|
@@ -199,21 +190,24 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
199
190
|
* A send timeout does not cancel delivery.
|
|
200
191
|
* Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
|
|
201
192
|
*
|
|
202
|
-
* @returns
|
|
193
|
+
* @returns Rejects on failure.
|
|
203
194
|
*
|
|
204
195
|
* @example
|
|
205
196
|
* Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
|
|
206
197
|
*
|
|
207
198
|
* ```ts
|
|
208
|
-
*
|
|
209
|
-
* if (!result.ok) throw result.error;
|
|
199
|
+
* await popcorn.send("receiver", tuple(atom("ok"), "value"));
|
|
210
200
|
* ```
|
|
211
201
|
*
|
|
212
202
|
* @see {@link AnyValue}
|
|
213
203
|
* @see {@link atom}
|
|
214
204
|
* @see {@link tuple}
|
|
205
|
+
*
|
|
206
|
+
* @see {@link trySend} for a result instead of throwing on expected failures.
|
|
215
207
|
*/
|
|
216
|
-
send(rawTarget: string | Pid, payload?: AnyValue): Promise<
|
|
208
|
+
send(rawTarget: string | Pid, payload?: AnyValue): Promise<void>;
|
|
209
|
+
/** See {@link send}. */
|
|
210
|
+
trySend(rawTarget: string | Pid, payload?: AnyValue): Promise<Result<null>>;
|
|
217
211
|
private sendBridge;
|
|
218
212
|
/**
|
|
219
213
|
* Registers a handler for BEAM message payloads.
|
|
@@ -235,9 +229,27 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
235
229
|
private emit;
|
|
236
230
|
private completeCall;
|
|
237
231
|
private parseCallReply;
|
|
238
|
-
|
|
232
|
+
/**
|
|
233
|
+
* Calls a GenServer by registered name or {@link Pid} and waits for its reply.
|
|
234
|
+
*
|
|
235
|
+
* @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy (see `Popcorn.Proxy`).
|
|
236
|
+
* @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
|
|
237
|
+
*
|
|
238
|
+
* GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
|
|
239
|
+
* A call timeout does not cancel server work.
|
|
240
|
+
*/
|
|
241
|
+
tryCall(rawTarget: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<Result<AnyValue>>;
|
|
239
242
|
private callBridge;
|
|
240
|
-
|
|
243
|
+
/**
|
|
244
|
+
* Sends a cast through the proxy.
|
|
245
|
+
*
|
|
246
|
+
* Success confirms delivery to the proxy
|
|
247
|
+
* @see {@link tryCall}
|
|
248
|
+
* @param opts - `proxy` allows to select another registered proxy.
|
|
249
|
+
*/
|
|
250
|
+
tryCast(rawTarget: string | Pid, request?: AnyValue, opts?: {
|
|
251
|
+
proxy?: string;
|
|
252
|
+
}): Promise<Result<null>>;
|
|
241
253
|
private castBridge;
|
|
242
254
|
private nextCallId;
|
|
243
255
|
private runJs;
|
|
@@ -248,6 +260,7 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
248
260
|
private sendBridgeReply;
|
|
249
261
|
private jsWithCurrentEnv;
|
|
250
262
|
private reviveHandles;
|
|
263
|
+
private reviveValueTag;
|
|
251
264
|
/** Maps pids and `TrackedValue`s during encoding, collecting handles into
|
|
252
265
|
* `tracked` for the caller to register once encoding succeeds. */
|
|
253
266
|
private handleMapper;
|
|
Binary file
|
|
Binary file
|
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
type CreateModuleFn<Mod> = (overrides?: Partial<Mod>) => Promise<Mod>;
|
|
2
2
|
/**
|
|
3
|
-
* A
|
|
3
|
+
* A value exchanged with BEAM. Containers convert their contents recursively.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* - Integers become integers. Other finite numbers become floats.
|
|
7
|
-
* - Arrays become lists.
|
|
8
|
-
* - Plain objects become maps with binary string keys.
|
|
9
|
-
* - `null` and `undefined` become the `nil` atom.
|
|
10
|
-
* - `atom()` and `tuple()` create atoms and tuples. PIDs retain their BEAM identity.
|
|
11
|
-
*
|
|
12
|
-
* Cycles, class instances, functions, symbols, bigints, unsafe integers, and non-finite numbers cause `bridge:unserializable`.
|
|
5
|
+
* See [Sharing values across languages](values.html) for supported types and conversions.
|
|
13
6
|
*/
|
|
14
7
|
export type AnyValue = unknown;
|
|
15
8
|
declare const pidBrand: unique symbol;
|
package/dist/worker.mjs
CHANGED
|
@@ -222,7 +222,7 @@ function unreachable() {
|
|
|
222
222
|
function objectWithKeys(value, keys) {
|
|
223
223
|
if (value === null || typeof value !== "object")
|
|
224
224
|
return null;
|
|
225
|
-
if (value
|
|
225
|
+
if (Object.getPrototypeOf(value) !== Object.prototype)
|
|
226
226
|
return null;
|
|
227
227
|
const hasAllKeys = keys.every((k) => Object.hasOwn(value, k));
|
|
228
228
|
if (!hasAllKeys)
|