@swmansion/popcorn 0.2.0-rc.2 → 0.2.0-rc.3

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/dist/AtomVM.mjs CHANGED
@@ -56,7 +56,7 @@ if (ENVIRONMENT_IS_PTHREAD) {
56
56
 
57
57
  // --pre-jses are emitted after the Module integration code, so that they can
58
58
  // refer to Module (if they choose; they can also define Module)
59
- // include: /tmp/popcorn-swm.J66P2Dx/atomvm/src/platforms/emscripten/src/atomvm.pre.js
59
+ // include: /tmp/popcorn-swm.0jor1PP/atomvm/src/platforms/emscripten/src/atomvm.pre.js
60
60
  /*
61
61
  * This file is part of AtomVM.
62
62
  *
@@ -128,7 +128,7 @@ function ensureValidResult(result) {
128
128
  throw new Error(message);
129
129
  }
130
130
 
131
- // end include: /tmp/popcorn-swm.J66P2Dx/atomvm/src/platforms/emscripten/src/atomvm.pre.js
131
+ // end include: /tmp/popcorn-swm.0jor1PP/atomvm/src/platforms/emscripten/src/atomvm.pre.js
132
132
  var arguments_ = [];
133
133
 
134
134
  var thisProgram = "./this.program";
@@ -7366,19 +7366,19 @@ function checkIncomingModuleAPI() {
7366
7366
  }
7367
7367
 
7368
7368
  var ASM_CONSTS = {
7369
- 77551: ($0, $1) => {
7369
+ 77455: ($0, $1) => {
7370
7370
  promiseMap.get($0).resolve($1);
7371
7371
  },
7372
- 77587: ($0, $1) => {
7372
+ 77491: ($0, $1) => {
7373
7373
  promiseMap.get($0).reject($1);
7374
7374
  },
7375
- 77622: ($0, $1) => {
7375
+ 77526: ($0, $1) => {
7376
7376
  promiseMap.get($0).resolve(UTF8ToString($1));
7377
7377
  },
7378
- 77672: ($0, $1) => {
7378
+ 77576: ($0, $1) => {
7379
7379
  promiseMap.get($0).reject(UTF8ToString($1));
7380
7380
  },
7381
- 77721: $0 => {
7381
+ 77625: $0 => {
7382
7382
  Module["onTrackedObjectDelete"]($0);
7383
7383
  }
7384
7384
  };
package/dist/AtomVM.wasm CHANGED
Binary file
package/dist/bridge.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type IframeRequest, type IframeResponse, type AnySerializable } from "./types";
1
+ import type { IframeRequest, IframeResponse, AnySerializable } from "./types";
2
2
  export type IframeBridgeArgs = {
3
3
  container: HTMLElement;
4
4
  config: Record<string, string>;
package/dist/bridge.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isMessageType } from './types.mjs';
2
- import { throwError } from './utils.mjs';
2
+ import { throwError } from './errors.mjs';
3
3
 
4
4
  const STYLE_HIDDEN = "visibility: hidden; width: 0px; height: 0px; border: none";
5
5
  function sendIframeResponse(type, data) {
@@ -0,0 +1,42 @@
1
+ /** Error codes for recoverable errors returned in CallResult */
2
+ export type PopcornErrorCode = "timeout" | "deinitialized" | "reload";
3
+ /** Recoverable error returned in CallResult (never thrown) */
4
+ export declare class PopcornError extends Error {
5
+ readonly code: PopcornErrorCode;
6
+ constructor(code: PopcornErrorCode, message?: string);
7
+ }
8
+ /** Error codes for internal errors that indicate bugs or misuse */
9
+ export type PopcornInternalErrorCode = "assert" | "private_constructor" | "bad_call" | "no_acked_call" | "bad_ack" | "already_awaited" | "already_mounted" | "unmounted" | "bad_target" | "bad_status";
10
+ /** Non-recoverable error indicating a bug or library misuse (always thrown) */
11
+ export declare class PopcornInternalError extends Error {
12
+ readonly code: PopcornInternalErrorCode;
13
+ constructor(code: PopcornInternalErrorCode, message?: string);
14
+ }
15
+ /** Internal errors - indicate bugs, protocol violations, or library misuse */
16
+ type ErrorData = {
17
+ t: "assert";
18
+ } | {
19
+ t: "private_constructor";
20
+ } | {
21
+ t: "bad_call";
22
+ } | {
23
+ t: "no_acked_call";
24
+ } | {
25
+ t: "bad_ack";
26
+ } | {
27
+ t: "already_awaited";
28
+ messageType: string;
29
+ awaitedMessageType: string;
30
+ } | {
31
+ t: "already_mounted";
32
+ } | {
33
+ t: "unmounted";
34
+ } | {
35
+ t: "bad_target";
36
+ } | {
37
+ t: "bad_status";
38
+ status: string;
39
+ expectedStatus: string;
40
+ };
41
+ export declare function throwError(error: ErrorData): never;
42
+ export {};
@@ -0,0 +1,49 @@
1
+ const defaultErrorMessages = {
2
+ timeout: "Promise timeout",
3
+ deinitialized: "Call cancelled due to instance deinit",
4
+ reload: "Call cancelled due to iframe reload",
5
+ };
6
+ /** Recoverable error returned in CallResult (never thrown) */
7
+ class PopcornError extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message ?? defaultErrorMessages[code]);
11
+ this.code = code;
12
+ this.name = "PopcornError";
13
+ }
14
+ }
15
+ /** Non-recoverable error indicating a bug or library misuse (always thrown) */
16
+ class PopcornInternalError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message ?? `Internal error: ${code}`);
20
+ this.code = code;
21
+ this.name = "PopcornInternalError";
22
+ }
23
+ }
24
+ function throwError(error) {
25
+ switch (error.t) {
26
+ case "assert":
27
+ throw new PopcornInternalError("assert", "Assertion error");
28
+ case "private_constructor":
29
+ throw new PopcornInternalError("private_constructor", "Don't construct the Popcorn object directly, use Popcorn.init() instead");
30
+ case "bad_call":
31
+ throw new PopcornInternalError("bad_call", "Response for non-existent call");
32
+ case "no_acked_call":
33
+ throw new PopcornInternalError("no_acked_call", "Response for non-acknowledged call");
34
+ case "bad_ack":
35
+ throw new PopcornInternalError("bad_ack", "Ack for non-existent call");
36
+ case "already_awaited":
37
+ throw new PopcornInternalError("already_awaited", `Cannot await message "${error.messageType}" when message "${error.awaitedMessageType}" is already awaited`);
38
+ case "already_mounted":
39
+ throw new PopcornInternalError("already_mounted", "Iframe already mounted");
40
+ case "unmounted":
41
+ throw new PopcornInternalError("unmounted", "WASM iframe not mounted");
42
+ case "bad_target":
43
+ throw new PopcornInternalError("bad_target", "Unspecified target process");
44
+ case "bad_status":
45
+ throw new PopcornInternalError("bad_status", `Operation not allowed: instance in "${error.status}" state, expected "${error.expectedStatus}"`);
46
+ }
47
+ }
48
+
49
+ export { PopcornError, PopcornInternalError, throwError };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { Popcorn, PopcornDeinitializedError } from "./popcorn";
2
- export type { CastOptions, CallOptions } from "./popcorn";
1
+ export { Popcorn, PopcornError, PopcornInternalError } from "./popcorn";
2
+ export type { PopcornErrorCode, PopcornInternalErrorCode, PopcornInitOptions, CastOptions, CallOptions, } from "./popcorn";
3
3
  export type { AnySerializable } from "./types";
package/dist/index.mjs CHANGED
@@ -1 +1,2 @@
1
- export { Popcorn, PopcornDeinitializedError } from './popcorn.mjs';
1
+ export { Popcorn } from './popcorn.mjs';
2
+ export { PopcornError, PopcornInternalError } from './errors.mjs';
package/dist/popcorn.d.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { type AnySerializable } from "./types";
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 };
2
6
  /** Options for Popcorn.init() */
3
7
  export type PopcornInitOptions = {
4
8
  /** DOM element to mount an iframe */
@@ -39,14 +43,12 @@ type CallResult = {
39
43
  } | {
40
44
  ok: false;
41
45
  /** Error from failed call */
42
- error?: AnySerializable;
46
+ error: Error;
43
47
  /** Amount of time it took to process the call */
44
48
  durationMs: number;
45
49
  };
46
50
  type LogType = "stdout" | "stderr";
47
51
  type LogListener = (message: string) => void;
48
- export declare class PopcornDeinitializedError extends Error {
49
- }
50
52
  /**
51
53
  * Manages Elixir by setting up iframe, WASM module, and event listeners. Used to sent messages to Elixir processes.
52
54
  */
@@ -127,4 +129,3 @@ export declare class Popcorn {
127
129
  private transition;
128
130
  private assertStatus;
129
131
  }
130
- export {};
package/dist/popcorn.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  import { IframeBridge } from './bridge.mjs';
2
- import { HEARTBEAT_TIMEOUT_MS, MESSAGES, MAX_RELOAD_N, CALL_TIMEOUT_MS, INIT_VM_TIMEOUT_MS } from './types.mjs';
3
- import { throwError } from './utils.mjs';
2
+ import { HEARTBEAT_TIMEOUT_MS, MESSAGES, MAX_RELOAD_N, INIT_VM_TIMEOUT_MS, CALL_TIMEOUT_MS } from './types.mjs';
3
+ import { throwError, PopcornError } from './errors.mjs';
4
+ export { PopcornInternalError } from './errors.mjs';
4
5
 
5
- class PopcornDeinitializedError extends Error {
6
- }
7
6
  const INIT_TOKEN = Symbol();
8
7
  const IFRAME_URL = new URL("./iframe.mjs", import.meta.url).href;
9
8
  /**
@@ -76,7 +75,15 @@ class Popcorn {
76
75
  await this.awaitMessage(MESSAGES.INIT);
77
76
  this.transition({ status: "await_vm" });
78
77
  this.trace("Main: iframe loaded");
79
- this.initProcess = await withTimeout(this.awaitMessage(MESSAGES.START_VM), INIT_VM_TIMEOUT_MS);
78
+ const startTime = performance.now();
79
+ const startVmResult = await withTimeout(this.awaitMessage(MESSAGES.START_VM).then((data) => ({
80
+ ok: true,
81
+ data,
82
+ durationMs: performance.now() - startTime,
83
+ })), INIT_VM_TIMEOUT_MS);
84
+ if (!startVmResult.ok)
85
+ throwError({ t: "assert" });
86
+ this.initProcess = startVmResult.data;
80
87
  this.transition({ status: "ready" });
81
88
  this.trace("Main: mounted, main process: ", this.initProcess);
82
89
  this.onHeartbeat();
@@ -105,7 +112,8 @@ class Popcorn {
105
112
  if (targetProcess === null)
106
113
  throwError({ t: "bad_target" });
107
114
  const requestId = this.requestId++;
108
- const callPromise = new Promise((resolve, reject) => {
115
+ const startTimeMs = performance.now();
116
+ const callPromise = new Promise((resolve) => {
109
117
  if (this.bridge === null)
110
118
  throwError({ t: "unmounted" });
111
119
  this.trace("Main: call: ", { requestId, process, args });
@@ -115,9 +123,8 @@ class Popcorn {
115
123
  });
116
124
  this.calls.set(requestId, {
117
125
  acknowledged: false,
118
- startTimeMs: performance.now(),
126
+ startTimeMs,
119
127
  resolve,
120
- reject,
121
128
  });
122
129
  });
123
130
  const result = await withTimeout(callPromise, timeoutMs ?? CALL_TIMEOUT_MS);
@@ -163,8 +170,9 @@ class Popcorn {
163
170
  this.logListeners.stderr.clear();
164
171
  for (const callData of this.calls.values()) {
165
172
  const durationMs = performance.now() - callData.startTimeMs;
166
- callData.reject({
167
- error: new PopcornDeinitializedError("Call cancelled due to instance deinit"),
173
+ callData.resolve({
174
+ ok: false,
175
+ error: new PopcornError("deinitialized"),
168
176
  durationMs,
169
177
  });
170
178
  }
@@ -251,10 +259,9 @@ class Popcorn {
251
259
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
252
260
  }, this.heartbeatTimeoutMs);
253
261
  }
254
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
255
262
  reloadIframe(reason = "other") {
256
263
  if (this.bridge === null) {
257
- throw new Error("WASM iframe not mounted for reload");
264
+ throwError({ t: "unmounted" });
258
265
  }
259
266
  if (document.hidden) {
260
267
  this.trace("Main: reloading iframe skipped, window not visible");
@@ -276,8 +283,9 @@ class Popcorn {
276
283
  }
277
284
  for (const callData of this.calls.values()) {
278
285
  const durationMs = performance.now() - callData.startTimeMs;
279
- callData.reject({
280
- error: new Error("Call cancelled due to iframe reload"),
286
+ callData.resolve({
287
+ ok: false,
288
+ error: new PopcornError("reload"),
281
289
  durationMs,
282
290
  });
283
291
  }
@@ -322,9 +330,13 @@ class Popcorn {
322
330
  }
323
331
  async function withTimeout(promise, ms) {
324
332
  let timeout = null;
325
- const timeoutPromise = new Promise((_resolve, reject) => {
333
+ const timeoutPromise = new Promise((resolve) => {
326
334
  timeout = setTimeout(() => {
327
- reject("Promise timeout");
335
+ resolve({
336
+ ok: false,
337
+ error: new PopcornError("timeout"),
338
+ durationMs: ms,
339
+ });
328
340
  }, ms);
329
341
  });
330
342
  const result = await Promise.race([promise, timeoutPromise]);
@@ -337,4 +349,4 @@ function noop() {
337
349
  /* noop */
338
350
  }
339
351
 
340
- export { Popcorn, PopcornDeinitializedError };
352
+ export { Popcorn, PopcornError };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/popcorn",
3
- "version": "0.2.0-rc.2",
3
+ "version": "0.2.0-rc.3",
4
4
  "description": "JS bindings for Popcorn",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "devDependencies": {
58
58
  "@eslint/js": "^9.39.0",
59
+ "@playwright/test": "^1.52.0",
59
60
  "@rollup/plugin-typescript": "^12.1.2",
60
61
  "@types/node": "^25.0.8",
61
62
  "esbuild": "^0.25.0",
@@ -70,10 +71,11 @@
70
71
  "vitest": "^4.0.17"
71
72
  },
72
73
  "scripts": {
73
- "setup:dev": "npm run assets:dev",
74
+ "setup:dev": "npm run assets:dev && rollup -c",
74
75
  "dev": "rollup -c --watch",
75
76
  "lint": "tsc --noEmit && eslint && prettier . --check --log-level=warn",
76
77
  "test": "vitest",
78
+ "test:e2e": "playwright test --config e2e/playwright.config.ts",
77
79
  "build:prod": "pnpm run lint && (rm -rf assets || true) && (rm -rf dist || true) && pnpm run assets:prod && rollup -c",
78
80
  "assets:dev": "./scripts/get_atomvm.sh assets/",
79
81
  "assets:prod": "RUNTIME_SOURCE='https://github.com/software-mansion-labs/FissionVM.git#swm' ./scripts/get_atomvm.sh assets/"
package/dist/utils.d.ts DELETED
@@ -1,27 +0,0 @@
1
- type ErrorData = {
2
- t: "assert";
3
- } | {
4
- t: "bad_status";
5
- status: string;
6
- expectedStatus: string;
7
- } | {
8
- t: "private_constructor";
9
- } | {
10
- t: "bad_call";
11
- } | {
12
- t: "no_acked_call";
13
- } | {
14
- t: "bad_ack";
15
- } | {
16
- t: "unmounted";
17
- } | {
18
- t: "bad_target";
19
- } | {
20
- t: "already_awaited";
21
- messageType: string;
22
- awaitedMessageType: string;
23
- } | {
24
- t: "already_mounted";
25
- };
26
- export declare function throwError(error: ErrorData): never;
27
- export {};
package/dist/utils.mjs DELETED
@@ -1,26 +0,0 @@
1
- function throwError(error) {
2
- switch (error.t) {
3
- case "assert":
4
- throw new Error("Assertion error");
5
- case "bad_status":
6
- throw new Error(`Unexpected status transition. Instance in ${error.status} status, expected ${error.expectedStatus}`);
7
- case "private_constructor":
8
- throw new Error("Don't construct the Popcorn object directly, use Popcorn.init() instead");
9
- case "bad_call":
10
- throw new Error("Response for non-existent call");
11
- case "no_acked_call":
12
- throw new Error("Response for non-acknowledged call");
13
- case "bad_ack":
14
- throw new Error("Ack for non-existent call");
15
- case "unmounted":
16
- throw new Error("WASM iframe not mounted");
17
- case "bad_target":
18
- throw new Error("Unspecified target process");
19
- case "already_awaited":
20
- throw new Error(`Cannot await message ${error.messageType} when a message ${error.awaitedMessageType} is already awaited on`);
21
- case "already_mounted":
22
- throw new Error("Iframe already mounted");
23
- }
24
- }
25
-
26
- export { throwError };