@swmansion/popcorn 0.3.2 → 0.4.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +12 -0
  3. package/README.md +78 -2
  4. package/dist/beam.d.ts +10 -0
  5. package/dist/errors.d.ts +96 -39
  6. package/dist/etf.d.ts +38 -0
  7. package/dist/events.d.ts +82 -0
  8. package/dist/index.d.ts +7 -3
  9. package/dist/index.mjs +1287 -2
  10. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/beam_patcher.ex +184 -0
  11. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/cli.ex +74 -0
  12. package/dist/plugins/beam_tools/lib/popcorn/beam_tools/packager.ex +540 -0
  13. package/dist/plugins/beam_tools/mix.exs +16 -0
  14. package/dist/plugins/beam_tools/patches/kernel/prim_tty.erl +13 -0
  15. package/dist/plugins/beam_tools/patches/stdlib/beam_lib.erl +27 -0
  16. package/dist/plugins/esbuild.d.ts +10 -2
  17. package/dist/plugins/esbuild.mjs +39 -34
  18. package/dist/plugins/rollup.d.ts +10 -2
  19. package/dist/plugins/rollup.mjs +46 -25
  20. package/dist/plugins/shared.d.ts +54 -4
  21. package/dist/plugins/shared.mjs +207 -0
  22. package/dist/plugins/vite.d.ts +17 -2
  23. package/dist/plugins/vite.mjs +201 -75
  24. package/dist/popcorn.d.ts +237 -110
  25. package/dist/runtimes/core/beam.emu.mjs +141 -0
  26. package/dist/runtimes/core/beam.mjs +141 -0
  27. package/dist/runtimes/core/beam.wasm +0 -0
  28. package/dist/runtimes/core/manifest.json +1 -0
  29. package/dist/runtimes/crypto/beam.emu.mjs +520 -0
  30. package/dist/runtimes/crypto/beam.mjs +520 -0
  31. package/dist/runtimes/crypto/beam.wasm +0 -0
  32. package/dist/runtimes/crypto/manifest.json +1 -0
  33. package/dist/tar.d.ts +4 -0
  34. package/dist/types.d.ts +108 -63
  35. package/dist/utils.d.ts +7 -0
  36. package/dist/worker.d.ts +1 -0
  37. package/dist/worker.mjs +765 -0
  38. package/package.json +20 -28
  39. package/dist/AtomVM.mjs +0 -7992
  40. package/dist/AtomVM.wasm +0 -0
  41. package/dist/bridge.d.ts +0 -22
  42. package/dist/bridge.mjs +0 -66
  43. package/dist/errors.mjs +0 -55
  44. package/dist/iframe.d.ts +0 -1
  45. package/dist/iframe.mjs +0 -215
  46. package/dist/popcorn.mjs +0 -381
  47. package/dist/types.mjs +0 -25
package/dist/popcorn.d.ts CHANGED
@@ -1,138 +1,265 @@
1
- import { PopcornError, PopcornInternalError } from "./errors";
2
- import type { AnySerializable } from "./types";
3
- import type { PopcornErrorCode, PopcornInternalErrorCode } from "./errors";
4
- export { PopcornError, PopcornInternalError };
5
- export type { PopcornErrorCode, PopcornInternalErrorCode };
6
- /** Options for Popcorn.init() */
7
- export type PopcornInitOptions = {
8
- /** DOM element to mount an iframe */
9
- container?: HTMLElement;
10
- /** Paths to compiled Elixir bundles (`.avm` files). */
11
- bundlePaths?: string[];
12
- /** Handler for stderr messages. */
13
- onStderr?: (message: string) => void;
14
- /** Handler for stdout messages. */
15
- onStdout?: (message: string) => void;
16
- /** Handler called when Popcorn reloads due to iframe crash */
17
- onReload?: (reason: string) => void;
18
- /** Heartbeat timeout in milliseconds. If an iframe doesn't respond within this time, it is reloaded. */
19
- heartbeatTimeoutMs?: number;
20
- /** Directory containing Wasm and scripts used inside iframe. */
21
- wasmDir?: string;
22
- /** Enable debug logging. */
23
- debug?: boolean;
1
+ import { type Result } from "./errors";
2
+ import { type PopcornEvent } from "./events";
3
+ import type { AnyValue, BeamBootOptions, OtpErrorPayload, Pid, TtySize } from "./types";
4
+ /** Output type for a terminal. */
5
+ type TtyOutput = "text" | "bytes";
6
+ type OutputChunk<Output extends TtyOutput> = Output extends "bytes" ? Uint8Array : string;
7
+ /** Browser VM configuration. */
8
+ export type PopcornOpts<Output extends TtyOutput = "text"> = {
9
+ beam?: Pick<BeamBootOptions, "emulatorArgs" | "extraArgs" | "env"> & {
10
+ /**
11
+ * Asset directory URL.
12
+ *
13
+ * Must end with `/`. Defaults to `otp/` next to the worker.
14
+ */
15
+ otpAssetsRoot?: string;
16
+ };
17
+ tty?: {
18
+ /** Initial terminal size. Defaults to 80 columns and 24 rows. */
19
+ size?: TtySize;
20
+ /** Output callback format. Defaults to `text` with streamed UTF-8 decoding. Use `bytes` for raw chunks. */
21
+ output?: Output;
22
+ };
23
+ timeoutsMs?: {
24
+ /**
25
+ * Maximum wait for the VM bridge.
26
+ *
27
+ * Defaults to 10 000 ms.
28
+ */
29
+ boot?: number;
30
+ /**
31
+ * Maximum wait for entrypoint startup after bridge readiness.
32
+ *
33
+ * Defaults to 60 000 ms.
34
+ */
35
+ appStartup?: number;
36
+ /**
37
+ * Maximum wait for a send resolving its process target.
38
+ *
39
+ * Defaults to 5 000 ms.
40
+ */
41
+ send?: number;
42
+ };
43
+ /**
44
+ * Receives stdout.
45
+ *
46
+ * Defaults to `console.log`.
47
+ * When `tty.output` is "bytes", we pass an `ArrayBuffer` as an argument and `string` otherwise
48
+ */
49
+ onStdout?: (chunk: OutputChunk<Output>) => void;
50
+ /**
51
+ * Receives stderr.
52
+ *
53
+ * Defaults to `console.error`.
54
+ * When `tty.output` is "bytes", we pass an `ArrayBuffer` as an argument and `string` otherwise
55
+ */
56
+ onStderr?: (chunk: OutputChunk<Output>) => void;
57
+ /**
58
+ * Receives VM errors and exits before shutdown.
59
+ *
60
+ * Defaults to console output.
61
+ */
62
+ onError?: (event: OtpErrorPayload) => void;
63
+ /**
64
+ * Module worker URL.
65
+ *
66
+ * Defaults to the worker included with the package.
67
+ */
68
+ workerUrl?: string | URL;
24
69
  };
25
- /** Options for cast method */
26
- export type CastOptions = {
27
- /** Receiver process name. */
28
- process?: string;
70
+ type VmExitReason = {
71
+ reason: "deinit";
72
+ } | {
73
+ reason: "abort";
74
+ data: string;
75
+ } | {
76
+ reason: "error";
77
+ data: string;
78
+ } | {
79
+ reason: "exit";
80
+ data: number;
29
81
  };
30
- /** Options for call method */
31
- export type CallOptions = {
32
- /** Registered Elixir process name. */
33
- process?: string;
34
- /** Timeout (in milliseconds) for the call */
82
+ type CallOpts = {
35
83
  timeoutMs?: number;
84
+ proxy?: string;
36
85
  };
37
- type CallResult = {
38
- ok: true;
39
- /** Serialized value returned from Elixir */
40
- data: AnySerializable;
41
- /** Amount of time it took to process the call */
42
- durationMs: number;
43
- } | {
44
- ok: false;
45
- /** Error from failed call */
46
- error: Error;
47
- /** Amount of time it took to process the call */
48
- durationMs: number;
86
+ export type GenServer = {
87
+ /**
88
+ * Calls a GenServer by registered name or {@link Pid} and waits for its reply.
89
+ *
90
+ * Requires a running `Popcorn.Proxy`, registered as `popcorn_proxy` by default.
91
+ *
92
+ * @param opts - `timeoutMs` defaults to 5 000 ms. `proxy` allows to select another registered proxy.
93
+ * @returns A {@link Result} with the reply or a bridge, VM, timeout, or GenServer error.
94
+ *
95
+ * GenServer failures use `genserver:noproc`, `genserver:exit`, or `genserver:unserializable`.
96
+ * A call timeout does not cancel server work.
97
+ */
98
+ call(target: string | Pid, request?: AnyValue, opts?: CallOpts): Promise<Result<AnyValue>>;
99
+ /**
100
+ * Sends a cast through the proxy.
101
+ *
102
+ * Success confirms delivery to the proxy
103
+ * @see {@link call}
104
+ * @param opts - `proxy` allows to select another registered proxy.
105
+ */
106
+ cast(target: string | Pid, request?: AnyValue, opts?: {
107
+ proxy?: string;
108
+ }): Promise<Result<null>>;
49
109
  };
50
- type LogType = "stdout" | "stderr";
51
- type LogListener = (message: string) => void;
52
- type MessageHandler = (eventName: string, payload: AnySerializable) => void;
53
110
  /**
54
- * Manages Elixir by setting up iframe, WASM module, and event listeners. Used to sent messages to Elixir processes.
55
- */
56
- export declare class Popcorn {
57
- heartbeatTimeoutMs: number | null;
58
- private onReloadCallback;
59
- private bridge;
60
- private bridgeConfig;
61
- private debug;
62
- private bundleURLs;
111
+ * A BEAM VM in a browser worker.
112
+ *
113
+ * Use {@link Popcorn.init} to create and start an instance.
114
+ **/
115
+ export declare class Popcorn<Output extends TtyOutput = "text"> {
116
+ private vmWorker;
63
117
  private state;
64
- private defaultReceiver;
65
- private requestId;
66
- private calls;
67
- private logListeners;
68
- private messageHandlers;
69
- private mountResolve;
70
- private heartbeatTimeout;
71
- private reloadN;
72
- private constructor();
118
+ private readonly opts;
119
+ private readonly ttySize;
120
+ private output;
121
+ private requestSeq;
122
+ private settleBoot;
123
+ private readonly eventHandlers;
124
+ private readonly pendingSends;
125
+ private readonly pendingCalls;
126
+ private callSeq;
127
+ private readonly trackedValues;
128
+ private trackedKeySeq;
129
+ private io;
130
+ private vmReady;
131
+ readonly genserver: GenServer;
132
+ private readonly TrackedValue;
133
+ private Pid;
134
+ private readonly onWorkerMessage;
73
135
  /**
74
- * Creates an iframe and sets up communication channels.
75
- * Returns after the Elixir app calls `Popcorn.Wasm.ready/0,1`.
136
+ * Creates the worker.
76
137
  *
77
- * @example
78
- * import { Popcorn } from "@swmansion/popcorn";
79
- * const popcorn = await Popcorn.init({
80
- * onStdout: console.log,
81
- * onStderr: console.error,
82
- * debug: true,
83
- * });
138
+ * Call {@link boot} to start the VM.
139
+ **/
140
+ constructor(opts: PopcornOpts<Output>);
141
+ private spawnWorker;
142
+ /**
143
+ * Creates an instance and waits for {@link boot}.
144
+ *
145
+ * For startup messages, use the constructor and register {@link onEvent} before boot.
146
+ *
147
+ * @returns Ok tuple or `runtime:eval-unavailable` if the page blocks JavaScript evaluation.
84
148
  */
85
- static init(options: PopcornInitOptions): Promise<Popcorn>;
86
- private mount;
149
+ static init<Output extends TtyOutput = "text">(opts: PopcornOpts<Output>): Promise<Result<Popcorn<Output>>>;
87
150
  /**
88
- * Sends a message to an Elixir process and awaits for the response.
151
+ * Starts the VM and waits for its bridge and entrypoint application.
89
152
  *
90
- * If Elixir doesn't respond in configured timeout, the returned promise will be rejected with "process timeout" error.
153
+ * Without an entrypoint, waits only for the bridge.
91
154
  *
92
- * Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
93
- * Throws "Unspecified target process" if default process is not set and no process is specified.
155
+ * After shutdown, starts a fresh VM with the original options.
156
+ *
157
+ * @returns Ok tuple with `this` if boot completes or error tuple.
94
158
  *
95
159
  * @example
96
- * const result = await popcorn.call(
97
- * { action: "get_user", id: 123 },
98
- * { process: "user_server", timeoutMs: 5_000 },
99
- * );
100
- * console.log(result.data); // Deserialized Elixir response
101
- * console.log(result.durationMs); // Entire call duration
160
+ * ```ts
161
+ * const popcorn = new Popcorn({});
162
+ * popcorn.onEvent((message) => console.log(message));
163
+ * const result = await popcorn.boot();
164
+ * if (!result.ok) throw result.error;
165
+ * ```
102
166
  */
103
- call(args: AnySerializable, { process, timeoutMs }?: CallOptions): Promise<CallResult>;
167
+ boot(): Promise<Result<Popcorn<Output>>>;
104
168
  /**
105
- * Sends a message to an Elixir process (default or from options) and returns immediately.
169
+ * Queues terminal input.
170
+ *
171
+ * Encodes strings as UTF-8 and copies byte arrays. Does not append a newline.
106
172
  *
107
- * Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
108
- * Throws "Unspecified target process" if default process is not set and no process is specified.
173
+ * Returns `stdio:overflow` if the chunk exceeds the remaining 64 KiB queue capacity.
174
+ * An overflow leaves the queue unchanged.
109
175
  */
110
- cast(args: AnySerializable, { process }?: CastOptions): void;
176
+ writeStdin(chunk: string | Uint8Array): Result<null>;
111
177
  /**
112
- * Destroys an iframe and resets the instance.
178
+ * Sends new terminal dimensions to a booted VM.
179
+ *
180
+ * Each dimension must be between 1 and 65,535.
113
181
  */
114
- deinit(): void;
115
- private teardownBridge;
182
+ resizeTty(columns: number, rows: number): Result<null>;
116
183
  /**
117
- * Registers a log listener that will be called when output of the specified type is received.
184
+ * Sends a payload to a registered process name or a {@link Pid} from this VM boot.
185
+ *
186
+ * The process receives `{wasm, Payload}`.
187
+ * A send timeout does not cancel delivery.
188
+ * Uses the value conversions in {@link AnyValue}. An omitted, `null`, or `undefined` payload becomes an empty map.
189
+ *
190
+ * @returns Ok tuple or `bridge:not-started` before boot and `vm:exited` after shutdown.
191
+ *
192
+ * @example
193
+ * Send an Erlang `{ok, <<"value">>}` tuple to a registered `receiver` process.
194
+ *
195
+ * ```ts
196
+ * const result = await popcorn.send("receiver", tuple(atom("ok"), "value"));
197
+ * if (!result.ok) throw result.error;
198
+ * ```
199
+ *
200
+ * @see {@link AnyValue}
201
+ * @see {@link atom}
202
+ * @see {@link tuple}
118
203
  */
119
- registerLogListener(listener: LogListener, type: LogType): void;
204
+ send(rawTarget: string | Pid, payload?: AnyValue): Promise<Result<null>>;
205
+ private sendBridge;
120
206
  /**
121
- * Unregisters a previously registered log listener.
207
+ * Registers a handler for BEAM message payloads.
208
+ *
209
+ * Messages with no handlers are lost. Startup messages can arrive before {@link boot} resolves.
210
+ * VM errors and terminal output use the callbacks in {@link PopcornOpts}.
211
+ *
212
+ * @returns a function that removes the handler.
122
213
  */
123
- unregisterLogListener(listener: LogListener, type: LogType): void;
124
- private notifyLogListeners;
214
+ onEvent(handler: (event: PopcornEvent) => void): () => void;
125
215
  /**
126
- * Registers a catch-all event handler. Returns an unsubscribe function.
216
+ * Stops the worker and completes pending sends and calls with `vm:exited`.
217
+ *
218
+ * Releases tracked values and runs their cleanup callbacks.
219
+ * Keeps event handlers for the next boot. Repeated calls have no effect.
127
220
  */
128
- onMessage(handler: MessageHandler): () => void;
129
- private onEvent;
130
- private iframeHandler;
131
- private onCallAck;
132
- private onCall;
133
- private onHeartbeat;
134
- private reloadIframe;
135
- trace(...messages: unknown[]): void;
136
- private transition;
137
- private assertStatus;
221
+ deinit(reason?: VmExitReason): void;
222
+ private clearTrackedValues;
223
+ private emit;
224
+ private completeCall;
225
+ private parseCallReply;
226
+ private call;
227
+ private callBridge;
228
+ private cast;
229
+ private castBridge;
230
+ private nextCallId;
231
+ private runJs;
232
+ private asRef;
233
+ private sendRunJsReply;
234
+ private jsWithCurrentEnv;
235
+ private reviveHandles;
236
+ /** Maps pids and `TrackedValue`s during encoding, collecting handles into
237
+ * `tracked` for the caller to register once encoding succeeds. */
238
+ private handleMapper;
239
+ private deleteTrackedValue;
240
+ private completeSend;
241
+ private nextRequestId;
242
+ private handleStdout;
243
+ private handleStderr;
244
+ private handleOtpError;
138
245
  }
246
+ /**
247
+ * Thread counts for {@link schedulers}.
248
+ *
249
+ * Each count must be positive.
250
+ */
251
+ export type SchedulerOptions = {
252
+ /** Regular schedulers. */
253
+ base: number;
254
+ /** Dirty CPU schedulers. */
255
+ dirtyCpu: number;
256
+ /** Dirty I/O schedulers. */
257
+ dirtyIo: number;
258
+ };
259
+ /**
260
+ * Builds `beam.emulatorArgs` for scheduler counts.
261
+ *
262
+ * Defaults to one scheduler of each type.
263
+ */
264
+ export declare function schedulers(opts: SchedulerOptions): string[];
265
+ export {};