@swmansion/popcorn 0.4.0-next.5 → 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 +281 -82
- package/dist/plugins/shared.d.ts +1 -19
- package/dist/plugins/shared.mjs +0 -20
- package/dist/popcorn.d.ts +53 -40
- 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,9 @@ 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);
|
|
639
771
|
if (opts.onEvent)
|
|
640
772
|
this.onEvent(opts.onEvent);
|
|
641
773
|
this.spawnWorker();
|
|
@@ -654,18 +786,16 @@ class Popcorn {
|
|
|
654
786
|
*
|
|
655
787
|
* Use `opts.onEvent` to receive messages sent during startup.
|
|
656
788
|
*
|
|
657
|
-
* @returns
|
|
789
|
+
* @returns The instance. Rejects on failure.
|
|
790
|
+
*
|
|
791
|
+
* @see {@link PopcornOpts}
|
|
658
792
|
*/
|
|
659
793
|
static async init(opts = {}) {
|
|
660
794
|
if (!canEval()) {
|
|
661
|
-
|
|
795
|
+
throw err$1("runtime:eval-unavailable", {});
|
|
662
796
|
}
|
|
663
797
|
const popcorn = new Popcorn(opts);
|
|
664
|
-
|
|
665
|
-
if (!result.ok) {
|
|
666
|
-
return result;
|
|
667
|
-
}
|
|
668
|
-
return { ok: true, data: popcorn };
|
|
798
|
+
return await popcorn.boot();
|
|
669
799
|
}
|
|
670
800
|
/**
|
|
671
801
|
* Starts the VM and waits for its bridge and entrypoint application.
|
|
@@ -674,37 +804,29 @@ class Popcorn {
|
|
|
674
804
|
*
|
|
675
805
|
* After shutdown, starts a fresh VM with the original options.
|
|
676
806
|
*
|
|
677
|
-
* @returns
|
|
807
|
+
* @returns This instance. Rejects on failure.
|
|
678
808
|
*
|
|
679
809
|
* @example
|
|
680
810
|
* ```ts
|
|
681
811
|
* const popcorn = new Popcorn({});
|
|
682
812
|
* popcorn.onEvent((message) => console.log(message));
|
|
683
|
-
*
|
|
684
|
-
* if (!result.ok) throw result.error;
|
|
813
|
+
* await popcorn.boot();
|
|
685
814
|
* ```
|
|
686
815
|
*/
|
|
687
816
|
async boot() {
|
|
688
|
-
if (this.state.status === "booted")
|
|
689
|
-
return
|
|
690
|
-
|
|
691
|
-
if (this.state.status === "booting") {
|
|
692
|
-
// TODO(jgonet): make it easier to construct check() errors without throwing
|
|
693
|
-
const error = err$1("internal:check", {
|
|
694
|
-
detail: "Boot already in progress",
|
|
695
|
-
});
|
|
696
|
-
return { ok: false, error };
|
|
697
|
-
}
|
|
817
|
+
if (this.state.status === "booted")
|
|
818
|
+
return this;
|
|
819
|
+
check(this.state.status !== "booting", "Boot already in progress");
|
|
698
820
|
const reboot = this.state.status === "closed";
|
|
699
821
|
if (reboot) {
|
|
700
822
|
this.spawnWorker();
|
|
701
823
|
}
|
|
702
824
|
this.Pid = createPidClass();
|
|
703
825
|
this.io = createIoState();
|
|
704
|
-
this.output = resolveOutputHandlers(this.opts);
|
|
826
|
+
this.output = resolveOutputHandlers(this.opts, this);
|
|
705
827
|
check(this.readyWaiters.length === 0);
|
|
706
828
|
this.state = { status: "booting" };
|
|
707
|
-
return await new Promise((resolve) => {
|
|
829
|
+
return await new Promise((resolve, reject) => {
|
|
708
830
|
const timeoutsMs = { ...DEFAULT_TIMEOUTS_MS, ...this.opts.timeoutsMs };
|
|
709
831
|
const settle = (result) => {
|
|
710
832
|
if (this.settleBoot === null)
|
|
@@ -714,7 +836,10 @@ class Popcorn {
|
|
|
714
836
|
if (!result.ok) {
|
|
715
837
|
this.deinit();
|
|
716
838
|
}
|
|
717
|
-
|
|
839
|
+
if (result.ok)
|
|
840
|
+
resolve(result.data);
|
|
841
|
+
else
|
|
842
|
+
reject(result.error);
|
|
718
843
|
};
|
|
719
844
|
this.settleBoot = settle;
|
|
720
845
|
const startPhase = (timeoutMs) => setTimeout(() => {
|
|
@@ -765,13 +890,12 @@ class Popcorn {
|
|
|
765
890
|
*
|
|
766
891
|
* Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
|
|
767
892
|
*
|
|
768
|
-
*
|
|
893
|
+
* Throws `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
|
|
769
894
|
* An overflow leaves the queue unchanged.
|
|
770
895
|
*/
|
|
771
896
|
writeStdin(chunk) {
|
|
772
|
-
if (this.state.status === "closed")
|
|
773
|
-
|
|
774
|
-
}
|
|
897
|
+
if (this.state.status === "closed")
|
|
898
|
+
throw this.state.error;
|
|
775
899
|
check(this.state.status === "booted");
|
|
776
900
|
const bytes = toBytes(chunk);
|
|
777
901
|
check(bytes.byteLength > 0);
|
|
@@ -781,29 +905,28 @@ class Popcorn {
|
|
|
781
905
|
capacityBytes: STDIN_QUEUE_CAPACITY_BYTES,
|
|
782
906
|
attemptedBytes,
|
|
783
907
|
});
|
|
784
|
-
|
|
908
|
+
throw error;
|
|
785
909
|
}
|
|
786
910
|
this.io.stdin.reservedBytes = attemptedBytes;
|
|
787
911
|
const event = { type: "popcorn:stdin", payload: { chunk: bytes } };
|
|
788
912
|
toVm(this.vmWorker, event, [bytes.buffer]);
|
|
789
|
-
return { ok: true, data: null };
|
|
790
913
|
}
|
|
791
914
|
/**
|
|
792
915
|
* Sends new terminal dimensions to a booted VM.
|
|
793
916
|
*
|
|
794
917
|
* Each dimension must be between 1 and 65,535.
|
|
918
|
+
*
|
|
919
|
+
* Throws on failure.
|
|
795
920
|
*/
|
|
796
921
|
resizeTty(columns, rows) {
|
|
797
|
-
if (this.state.status === "closed")
|
|
798
|
-
|
|
799
|
-
}
|
|
922
|
+
if (this.state.status === "closed")
|
|
923
|
+
throw this.state.error;
|
|
800
924
|
check(this.state.status === "booted");
|
|
801
925
|
check(isValidTtySize({ columns, rows }));
|
|
802
926
|
toVm(this.vmWorker, {
|
|
803
927
|
type: "popcorn:tty-resize",
|
|
804
928
|
payload: { columns, rows },
|
|
805
929
|
});
|
|
806
|
-
return { ok: true, data: null };
|
|
807
930
|
}
|
|
808
931
|
/**
|
|
809
932
|
* Sends a payload to a registered process name or a {@link Pid} from this VM boot.
|
|
@@ -812,21 +935,26 @@ class Popcorn {
|
|
|
812
935
|
* A send timeout does not cancel delivery.
|
|
813
936
|
* Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
|
|
814
937
|
*
|
|
815
|
-
* @returns
|
|
938
|
+
* @returns Rejects on failure.
|
|
816
939
|
*
|
|
817
940
|
* @example
|
|
818
941
|
* Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
|
|
819
942
|
*
|
|
820
943
|
* ```ts
|
|
821
|
-
*
|
|
822
|
-
* if (!result.ok) throw result.error;
|
|
944
|
+
* await popcorn.send("receiver", tuple(atom("ok"), "value"));
|
|
823
945
|
* ```
|
|
824
946
|
*
|
|
825
947
|
* @see {@link AnyValue}
|
|
826
948
|
* @see {@link atom}
|
|
827
949
|
* @see {@link tuple}
|
|
950
|
+
*
|
|
951
|
+
* @see {@link trySend} for a result instead of throwing on expected failures.
|
|
828
952
|
*/
|
|
829
953
|
async send(rawTarget, payload) {
|
|
954
|
+
unwrap(await this.trySend(rawTarget, payload));
|
|
955
|
+
}
|
|
956
|
+
/** See {@link send}. */
|
|
957
|
+
async trySend(rawTarget, payload) {
|
|
830
958
|
if (this.state.status !== "booted") {
|
|
831
959
|
if (this.state.status === "closed") {
|
|
832
960
|
return { ok: false, error: this.state.error };
|
|
@@ -980,7 +1108,16 @@ class Popcorn {
|
|
|
980
1108
|
unreachable();
|
|
981
1109
|
}
|
|
982
1110
|
}
|
|
983
|
-
|
|
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) {
|
|
984
1121
|
if (this.state.status !== "booted") {
|
|
985
1122
|
if (this.state.status === "closed") {
|
|
986
1123
|
return { ok: false, error: this.state.error };
|
|
@@ -1023,7 +1160,14 @@ class Popcorn {
|
|
|
1023
1160
|
}
|
|
1024
1161
|
return result;
|
|
1025
1162
|
}
|
|
1026
|
-
|
|
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) {
|
|
1027
1171
|
if (this.state.status !== "booted") {
|
|
1028
1172
|
if (this.state.status === "closed") {
|
|
1029
1173
|
return { ok: false, error: this.state.error };
|
|
@@ -1052,9 +1196,16 @@ class Popcorn {
|
|
|
1052
1196
|
const args = this.reviveHandles(request.args);
|
|
1053
1197
|
check(this.vmReady);
|
|
1054
1198
|
const actions = {
|
|
1055
|
-
send: (target, payload) =>
|
|
1056
|
-
|
|
1057
|
-
|
|
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),
|
|
1058
1209
|
};
|
|
1059
1210
|
const result = await fn(args, actions);
|
|
1060
1211
|
const value = request.return === "ref" ? this.asRef(result) : result;
|
|
@@ -1112,6 +1263,10 @@ class Popcorn {
|
|
|
1112
1263
|
return make(this.TrackedValue);
|
|
1113
1264
|
}
|
|
1114
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
|
+
}
|
|
1115
1270
|
const key = trackedRefKey(value);
|
|
1116
1271
|
if (key !== null) {
|
|
1117
1272
|
const entry = this.trackedValues.get(key);
|
|
@@ -1127,14 +1282,55 @@ class Popcorn {
|
|
|
1127
1282
|
}
|
|
1128
1283
|
const obj = objectWithKeys(value, []);
|
|
1129
1284
|
if (obj !== null) {
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
return
|
|
1285
|
+
const entries = Object.entries(obj).map(([key, item]) => [
|
|
1286
|
+
key,
|
|
1287
|
+
this.reviveHandles(item),
|
|
1288
|
+
]);
|
|
1289
|
+
return Object.fromEntries(entries);
|
|
1135
1290
|
}
|
|
1136
1291
|
return value;
|
|
1137
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
|
+
}
|
|
1138
1334
|
/** Maps pids and `TrackedValue`s during encoding, collecting handles into
|
|
1139
1335
|
* `tracked` for the caller to register once encoding succeeds. */
|
|
1140
1336
|
handleMapper(tracked) {
|
|
@@ -1230,22 +1426,20 @@ function isValidTtySize({ columns, rows }) {
|
|
|
1230
1426
|
const rowInRange = 0 < rows && rows <= 0xffff;
|
|
1231
1427
|
return colInRange && rowInRange;
|
|
1232
1428
|
}
|
|
1233
|
-
function resolveOutputHandlers(opts) {
|
|
1429
|
+
function resolveOutputHandlers(opts, callbacks) {
|
|
1234
1430
|
if (opts.tty?.output === "bytes") {
|
|
1235
|
-
const onStdout = opts.onStdout;
|
|
1236
|
-
const onStderr = opts.onStderr;
|
|
1237
1431
|
return {
|
|
1238
|
-
stdout: onStdout ??
|
|
1239
|
-
|
|
1432
|
+
stdout: (chunk) => (callbacks.onStdout ??
|
|
1433
|
+
defaultOnStdoutBytes)(chunk),
|
|
1434
|
+
stderr: (chunk) => (callbacks.onStderr ??
|
|
1435
|
+
defaultOnStderrBytes)(chunk),
|
|
1240
1436
|
};
|
|
1241
1437
|
}
|
|
1242
1438
|
const stdoutDecoder = new TextDecoder();
|
|
1243
1439
|
const stderrDecoder = new TextDecoder();
|
|
1244
|
-
const onStdout = opts.onStdout ?? defaultOnStdout;
|
|
1245
|
-
const onStderr = opts.onStderr ?? defaultOnStderr;
|
|
1246
1440
|
return {
|
|
1247
|
-
stdout: (chunk) => decodeOutput(stdoutDecoder, onStdout, chunk),
|
|
1248
|
-
stderr: (chunk) => decodeOutput(stderrDecoder, onStderr, chunk),
|
|
1441
|
+
stdout: (chunk) => decodeOutput(stdoutDecoder, callbacks.onStdout ?? defaultOnStdout, chunk),
|
|
1442
|
+
stderr: (chunk) => decodeOutput(stderrDecoder, callbacks.onStderr ?? defaultOnStderr, chunk),
|
|
1249
1443
|
};
|
|
1250
1444
|
}
|
|
1251
1445
|
function decodeOutput(decoder, onOutput, chunk) {
|
|
@@ -1335,5 +1529,10 @@ function defaultOnError(payload) {
|
|
|
1335
1529
|
unreachable();
|
|
1336
1530
|
}
|
|
1337
1531
|
}
|
|
1532
|
+
function readInteger(value) {
|
|
1533
|
+
check(typeof value === "string");
|
|
1534
|
+
check(/^-?(0|[1-9][0-9]*)$/.test(value));
|
|
1535
|
+
return BigInt(value);
|
|
1536
|
+
}
|
|
1338
1537
|
|
|
1339
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/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
|
/**
|
|
@@ -95,34 +95,17 @@ type CallOpts = {
|
|
|
95
95
|
timeoutMs?: number;
|
|
96
96
|
proxy?: string;
|
|
97
97
|
};
|
|
98
|
-
export type GenServer =
|
|
99
|
-
/**
|
|
100
|
-
* Calls a GenServer by registered name or {@link Pid} and waits for its reply.
|
|
101
|
-
*
|
|
102
|
-
* @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy (see `Popcorn.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;
|
|
@@ -156,9 +144,11 @@ export declare class Popcorn<Output extends TtyOutput = "text"> {
|
|
|
156
144
|
*
|
|
157
145
|
* Use `opts.onEvent` to receive messages sent during startup.
|
|
158
146
|
*
|
|
159
|
-
* @returns
|
|
147
|
+
* @returns The instance. Rejects on failure.
|
|
148
|
+
*
|
|
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)
|