@swmansion/popcorn 0.2.2 → 0.3.0-rc1
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 +267 -100
- package/dist/AtomVM.wasm +0 -0
- package/dist/bridge.mjs +5 -4
- package/dist/errors.d.ts +4 -5
- package/dist/errors.mjs +18 -14
- package/dist/iframe.d.ts +1 -1
- package/dist/iframe.mjs +6 -15
- package/dist/popcorn.d.ts +13 -6
- package/dist/popcorn.mjs +86 -82
- package/dist/types.d.ts +12 -7
- package/dist/types.mjs +7 -3
- package/package.json +1 -1
package/dist/AtomVM.wasm
CHANGED
|
Binary file
|
package/dist/bridge.mjs
CHANGED
|
@@ -41,10 +41,11 @@ class IframeBridge {
|
|
|
41
41
|
window.removeEventListener("message", this.handlerRef);
|
|
42
42
|
this.iframe.remove();
|
|
43
43
|
}
|
|
44
|
-
messageHandler(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
messageHandler(event) {
|
|
45
|
+
const fromOurIframe = event.source === this.iframe.contentWindow;
|
|
46
|
+
if (!fromOurIframe || !isIframeResponse(event.data))
|
|
47
|
+
return;
|
|
48
|
+
this.onMessage(event.data);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
function isIframeResponse(payload) {
|
package/dist/errors.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export declare class PopcornError extends Error {
|
|
|
6
6
|
constructor(code: PopcornErrorCode, message?: string);
|
|
7
7
|
}
|
|
8
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" | "
|
|
9
|
+
export type PopcornInternalErrorCode = "assert" | "private_constructor" | "bad_call" | "no_acked_call" | "bad_ack" | "already_mounted" | "unmounted" | "bad_target" | "bad_status" | "app_ready_timeout" | "bundle_not_found";
|
|
10
10
|
/** Non-recoverable error indicating a bug or library misuse (always thrown) */
|
|
11
11
|
export declare class PopcornInternalError extends Error {
|
|
12
12
|
readonly code: PopcornInternalErrorCode;
|
|
@@ -23,10 +23,6 @@ type ErrorData = {
|
|
|
23
23
|
t: "no_acked_call";
|
|
24
24
|
} | {
|
|
25
25
|
t: "bad_ack";
|
|
26
|
-
} | {
|
|
27
|
-
t: "already_awaited";
|
|
28
|
-
messageType: string;
|
|
29
|
-
awaitedMessageType: string;
|
|
30
26
|
} | {
|
|
31
27
|
t: "already_mounted";
|
|
32
28
|
} | {
|
|
@@ -37,10 +33,13 @@ type ErrorData = {
|
|
|
37
33
|
t: "bad_status";
|
|
38
34
|
status: string;
|
|
39
35
|
expectedStatus: string;
|
|
36
|
+
} | {
|
|
37
|
+
t: "app_ready_timeout";
|
|
40
38
|
} | {
|
|
41
39
|
t: "bundle_not_found";
|
|
42
40
|
primary: string;
|
|
43
41
|
fallback: string;
|
|
44
42
|
};
|
|
43
|
+
export declare function buildError(error: ErrorData): PopcornInternalError;
|
|
45
44
|
export declare function throwError(error: ErrorData): never;
|
|
46
45
|
export {};
|
package/dist/errors.mjs
CHANGED
|
@@ -12,6 +12,7 @@ class PopcornError extends Error {
|
|
|
12
12
|
this.name = "PopcornError";
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
const INIT_VM_TIMEOUT_MS = 30_000;
|
|
15
16
|
/** Non-recoverable error indicating a bug or library misuse (always thrown) */
|
|
16
17
|
class PopcornInternalError extends Error {
|
|
17
18
|
code;
|
|
@@ -21,31 +22,34 @@ class PopcornInternalError extends Error {
|
|
|
21
22
|
this.name = "PopcornInternalError";
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
function
|
|
25
|
+
function buildError(error) {
|
|
25
26
|
switch (error.t) {
|
|
26
27
|
case "assert":
|
|
27
|
-
|
|
28
|
+
return new PopcornInternalError("assert", "Assertion error");
|
|
28
29
|
case "private_constructor":
|
|
29
|
-
|
|
30
|
+
return new PopcornInternalError("private_constructor", "Don't construct the Popcorn object directly, use Popcorn.init() instead");
|
|
30
31
|
case "bad_call":
|
|
31
|
-
|
|
32
|
+
return new PopcornInternalError("bad_call", "Response for non-existent call");
|
|
32
33
|
case "no_acked_call":
|
|
33
|
-
|
|
34
|
+
return new PopcornInternalError("no_acked_call", "Response for non-acknowledged call");
|
|
34
35
|
case "bad_ack":
|
|
35
|
-
|
|
36
|
-
case "already_awaited":
|
|
37
|
-
throw new PopcornInternalError("already_awaited", `Cannot await message "${error.messageType}" when message "${error.awaitedMessageType}" is already awaited`);
|
|
36
|
+
return new PopcornInternalError("bad_ack", "Ack for non-existent call");
|
|
38
37
|
case "already_mounted":
|
|
39
|
-
|
|
38
|
+
return new PopcornInternalError("already_mounted", "Iframe already mounted");
|
|
40
39
|
case "unmounted":
|
|
41
|
-
|
|
40
|
+
return new PopcornInternalError("unmounted", "WASM iframe not mounted");
|
|
42
41
|
case "bad_target":
|
|
43
|
-
|
|
42
|
+
return new PopcornInternalError("bad_target", "Unspecified target process");
|
|
44
43
|
case "bad_status":
|
|
45
|
-
|
|
44
|
+
return new PopcornInternalError("bad_status", `Operation not allowed: instance in "${error.status}" state, expected "${error.expectedStatus}"`);
|
|
45
|
+
case "app_ready_timeout":
|
|
46
|
+
return new PopcornInternalError("app_ready_timeout", `Elixir app did not call Popcorn.Wasm.ready() within ${INIT_VM_TIMEOUT_MS}ms`);
|
|
46
47
|
case "bundle_not_found":
|
|
47
|
-
|
|
48
|
+
return new PopcornInternalError("bundle_not_found", `Could not find a valid .avm bundle at "${error.primary}" or fallback "${error.fallback}"`);
|
|
48
49
|
}
|
|
49
50
|
}
|
|
51
|
+
function throwError(error) {
|
|
52
|
+
throw buildError(error);
|
|
53
|
+
}
|
|
50
54
|
|
|
51
|
-
export { PopcornError, PopcornInternalError, throwError };
|
|
55
|
+
export { PopcornError, PopcornInternalError, buildError, throwError };
|
package/dist/iframe.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function initVm(): Promise<void>;
|
package/dist/iframe.mjs
CHANGED
|
@@ -2,8 +2,7 @@ import init from './AtomVM.mjs';
|
|
|
2
2
|
|
|
3
3
|
const HEARTBEAT_INTERVAL_MS = 500;
|
|
4
4
|
const MESSAGES = {
|
|
5
|
-
|
|
6
|
-
START_VM: "popcorn-startVm",
|
|
5
|
+
EVENT: "popcorn-event",
|
|
7
6
|
CALL: "popcorn-call",
|
|
8
7
|
CAST: "popcorn-cast",
|
|
9
8
|
CALL_ACK: "popcorn-callAck",
|
|
@@ -32,7 +31,7 @@ class TrackedValue {
|
|
|
32
31
|
}
|
|
33
32
|
}
|
|
34
33
|
globalThis.TrackedValue = TrackedValue;
|
|
35
|
-
async function
|
|
34
|
+
async function initVm() {
|
|
36
35
|
const metaElement = document.querySelector('meta[name="bundle-path"]');
|
|
37
36
|
if (!metaElement) {
|
|
38
37
|
throw new Error("Missing meta[name='bundle-path'] element");
|
|
@@ -40,8 +39,7 @@ async function runIFrame() {
|
|
|
40
39
|
const bundlePath = metaElement.content;
|
|
41
40
|
const bundleBuffer = await fetch(bundlePath).then((resp) => resp.arrayBuffer());
|
|
42
41
|
const bundle = new Int8Array(bundleBuffer);
|
|
43
|
-
|
|
44
|
-
const initProcess = await startVm(bundle);
|
|
42
|
+
await startVm(bundle);
|
|
45
43
|
window.addEventListener("message", async ({ data }) => {
|
|
46
44
|
const type = data.type;
|
|
47
45
|
if (type === MESSAGES.CALL) {
|
|
@@ -51,14 +49,9 @@ async function runIFrame() {
|
|
|
51
49
|
handleCast(data.value);
|
|
52
50
|
}
|
|
53
51
|
});
|
|
54
|
-
sendIframeResponse(MESSAGES.START_VM, initProcess);
|
|
55
52
|
setInterval(() => sendIframeResponse(MESSAGES.HEARTBEAT, null), HEARTBEAT_INTERVAL_MS);
|
|
56
53
|
}
|
|
57
54
|
async function startVm(avmBundle) {
|
|
58
|
-
let resolveResultPromise = null;
|
|
59
|
-
const resultPromise = new Promise((resolve) => {
|
|
60
|
-
resolveResultPromise = resolve;
|
|
61
|
-
});
|
|
62
55
|
const moduleInstance = await init({
|
|
63
56
|
preRun: [
|
|
64
57
|
function ({ FS }) {
|
|
@@ -152,11 +145,9 @@ async function startVm(avmBundle) {
|
|
|
152
145
|
};
|
|
153
146
|
return keys.map(getTrackedObject);
|
|
154
147
|
};
|
|
155
|
-
moduleInstance["
|
|
156
|
-
|
|
157
|
-
resolveResultPromise?.(initProcess);
|
|
148
|
+
moduleInstance["sendEvent"] = (eventName, payload) => {
|
|
149
|
+
sendIframeResponse(MESSAGES.EVENT, { eventName, payload });
|
|
158
150
|
};
|
|
159
|
-
return resultPromise;
|
|
160
151
|
}
|
|
161
152
|
async function handleCall(request) {
|
|
162
153
|
if (!Module) {
|
|
@@ -213,4 +204,4 @@ function deserialize(message) {
|
|
|
213
204
|
});
|
|
214
205
|
}
|
|
215
206
|
|
|
216
|
-
export {
|
|
207
|
+
export { initVm };
|
package/dist/popcorn.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ type CallResult = {
|
|
|
49
49
|
};
|
|
50
50
|
type LogType = "stdout" | "stderr";
|
|
51
51
|
type LogListener = (message: string) => void;
|
|
52
|
+
type MessageHandler = (eventName: string, payload: AnySerializable) => void;
|
|
52
53
|
/**
|
|
53
54
|
* Manages Elixir by setting up iframe, WASM module, and event listeners. Used to sent messages to Elixir processes.
|
|
54
55
|
*/
|
|
@@ -60,17 +61,18 @@ export declare class Popcorn {
|
|
|
60
61
|
private debug;
|
|
61
62
|
private bundleURL;
|
|
62
63
|
private state;
|
|
63
|
-
private
|
|
64
|
+
private defaultReceiver;
|
|
64
65
|
private requestId;
|
|
65
66
|
private calls;
|
|
66
67
|
private logListeners;
|
|
67
|
-
private
|
|
68
|
+
private messageHandlers;
|
|
69
|
+
private mountResolve;
|
|
68
70
|
private heartbeatTimeout;
|
|
69
71
|
private reloadN;
|
|
70
72
|
private constructor();
|
|
71
73
|
/**
|
|
72
74
|
* Creates an iframe and sets up communication channels.
|
|
73
|
-
* Returns after Elixir
|
|
75
|
+
* Returns after the Elixir app calls `Popcorn.Wasm.ready/0,1`.
|
|
74
76
|
*
|
|
75
77
|
* @example
|
|
76
78
|
* import { Popcorn } from "@swmansion/popcorn";
|
|
@@ -87,7 +89,7 @@ export declare class Popcorn {
|
|
|
87
89
|
*
|
|
88
90
|
* If Elixir doesn't respond in configured timeout, the returned promise will be rejected with "process timeout" error.
|
|
89
91
|
*
|
|
90
|
-
* Unless passed via options, the name passed in `Popcorn.Wasm.
|
|
92
|
+
* Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
|
|
91
93
|
* Throws "Unspecified target process" if default process is not set and no process is specified.
|
|
92
94
|
*
|
|
93
95
|
* @example
|
|
@@ -102,7 +104,7 @@ export declare class Popcorn {
|
|
|
102
104
|
/**
|
|
103
105
|
* Sends a message to an Elixir process (default or from options) and returns immediately.
|
|
104
106
|
*
|
|
105
|
-
* Unless passed via options, the name passed in `Popcorn.Wasm.
|
|
107
|
+
* Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
|
|
106
108
|
* Throws "Unspecified target process" if default process is not set and no process is specified.
|
|
107
109
|
*/
|
|
108
110
|
cast(args: AnySerializable, { process }?: CastOptions): void;
|
|
@@ -110,6 +112,7 @@ export declare class Popcorn {
|
|
|
110
112
|
* Destroys an iframe and resets the instance.
|
|
111
113
|
*/
|
|
112
114
|
deinit(): void;
|
|
115
|
+
private teardownBridge;
|
|
113
116
|
/**
|
|
114
117
|
* Registers a log listener that will be called when output of the specified type is received.
|
|
115
118
|
*/
|
|
@@ -119,12 +122,16 @@ export declare class Popcorn {
|
|
|
119
122
|
*/
|
|
120
123
|
unregisterLogListener(listener: LogListener, type: LogType): void;
|
|
121
124
|
private notifyLogListeners;
|
|
125
|
+
/**
|
|
126
|
+
* Registers a catch-all event handler. Returns an unsubscribe function.
|
|
127
|
+
*/
|
|
128
|
+
onMessage(handler: MessageHandler): () => void;
|
|
129
|
+
private onEvent;
|
|
122
130
|
private iframeHandler;
|
|
123
131
|
private onCallAck;
|
|
124
132
|
private onCall;
|
|
125
133
|
private onHeartbeat;
|
|
126
134
|
private reloadIframe;
|
|
127
|
-
private awaitMessage;
|
|
128
135
|
trace(...messages: unknown[]): void;
|
|
129
136
|
private transition;
|
|
130
137
|
private assertStatus;
|
package/dist/popcorn.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IframeBridge } from './bridge.mjs';
|
|
2
|
-
import { HEARTBEAT_TIMEOUT_MS, MESSAGES,
|
|
3
|
-
import { throwError, PopcornError } from './errors.mjs';
|
|
2
|
+
import { HEARTBEAT_TIMEOUT_MS, INIT_VM_TIMEOUT_MS, MESSAGES, EVENT_NAMES, MAX_RELOAD_N, CALL_TIMEOUT_MS } from './types.mjs';
|
|
3
|
+
import { throwError, buildError, PopcornError } from './errors.mjs';
|
|
4
4
|
export { PopcornInternalError } from './errors.mjs';
|
|
5
5
|
|
|
6
6
|
const INIT_TOKEN = Symbol();
|
|
@@ -16,14 +16,15 @@ class Popcorn {
|
|
|
16
16
|
debug = false;
|
|
17
17
|
bundleURL;
|
|
18
18
|
state = { status: "uninitialized" };
|
|
19
|
-
|
|
19
|
+
defaultReceiver = null;
|
|
20
20
|
requestId = 0;
|
|
21
21
|
calls = new Map();
|
|
22
22
|
logListeners = {
|
|
23
23
|
stdout: new Set(),
|
|
24
24
|
stderr: new Set(),
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
messageHandlers = new Set();
|
|
27
|
+
mountResolve = null;
|
|
27
28
|
heartbeatTimeout = null;
|
|
28
29
|
reloadN = 0;
|
|
29
30
|
constructor(params, token) {
|
|
@@ -36,7 +37,7 @@ class Popcorn {
|
|
|
36
37
|
this.bundleURL = bundleURL.href;
|
|
37
38
|
this.bridgeConfig = {
|
|
38
39
|
container: params.container,
|
|
39
|
-
script: { url: IFRAME_URL, entrypoint: "
|
|
40
|
+
script: { url: IFRAME_URL, entrypoint: "initVm" },
|
|
40
41
|
config: { "bundle-path": this.bundleURL },
|
|
41
42
|
debug: true,
|
|
42
43
|
onMessage: this.iframeHandler.bind(this),
|
|
@@ -47,7 +48,7 @@ class Popcorn {
|
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Creates an iframe and sets up communication channels.
|
|
50
|
-
* Returns after Elixir
|
|
51
|
+
* Returns after the Elixir app calls `Popcorn.Wasm.ready/0,1`.
|
|
51
52
|
*
|
|
52
53
|
* @example
|
|
53
54
|
* import { Popcorn } from "@swmansion/popcorn";
|
|
@@ -76,20 +77,19 @@ class Popcorn {
|
|
|
76
77
|
this.trace("Main: mount, container: ", this.bridgeConfig.container);
|
|
77
78
|
this.bridge = new IframeBridge(this.bridgeConfig);
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.initProcess = startVmResult.data;
|
|
80
|
+
const mountPromise = new Promise((resolve) => {
|
|
81
|
+
this.mountResolve = resolve;
|
|
82
|
+
});
|
|
83
|
+
let initTimeout;
|
|
84
|
+
await Promise.race([
|
|
85
|
+
mountPromise,
|
|
86
|
+
new Promise((_, reject) => {
|
|
87
|
+
initTimeout = setTimeout(() => reject(buildError({ t: "app_ready_timeout" })), INIT_VM_TIMEOUT_MS);
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
clearTimeout(initTimeout);
|
|
91
91
|
this.transition({ status: "ready" });
|
|
92
|
-
this.trace("Main: mounted
|
|
92
|
+
this.trace("Main: mounted");
|
|
93
93
|
this.onHeartbeat();
|
|
94
94
|
}
|
|
95
95
|
catch (error) {
|
|
@@ -102,7 +102,7 @@ class Popcorn {
|
|
|
102
102
|
*
|
|
103
103
|
* If Elixir doesn't respond in configured timeout, the returned promise will be rejected with "process timeout" error.
|
|
104
104
|
*
|
|
105
|
-
* Unless passed via options, the name passed in `Popcorn.Wasm.
|
|
105
|
+
* Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
|
|
106
106
|
* Throws "Unspecified target process" if default process is not set and no process is specified.
|
|
107
107
|
*
|
|
108
108
|
* @example
|
|
@@ -115,26 +115,20 @@ class Popcorn {
|
|
|
115
115
|
*/
|
|
116
116
|
async call(args, { process, timeoutMs } = {}) {
|
|
117
117
|
this.assertStatus(["ready"]);
|
|
118
|
-
const targetProcess = process ?? this.
|
|
119
|
-
if (this.bridge === null)
|
|
120
|
-
throwError({ t: "unmounted" });
|
|
118
|
+
const targetProcess = process ?? this.defaultReceiver;
|
|
121
119
|
if (targetProcess === null)
|
|
122
120
|
throwError({ t: "bad_target" });
|
|
121
|
+
if (this.bridge === null)
|
|
122
|
+
throwError({ t: "unmounted" });
|
|
123
123
|
const requestId = this.requestId++;
|
|
124
124
|
const startTimeMs = performance.now();
|
|
125
125
|
const callPromise = new Promise((resolve) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
this.calls.set(requestId, {
|
|
134
|
-
acknowledged: false,
|
|
135
|
-
startTimeMs,
|
|
136
|
-
resolve,
|
|
137
|
-
});
|
|
126
|
+
this.calls.set(requestId, { acknowledged: false, startTimeMs, resolve });
|
|
127
|
+
});
|
|
128
|
+
this.trace("Main: call: ", { requestId, process, args });
|
|
129
|
+
this.bridge.sendIframeRequest({
|
|
130
|
+
type: MESSAGES.CALL,
|
|
131
|
+
value: { requestId, process: targetProcess, args },
|
|
138
132
|
});
|
|
139
133
|
const result = await withTimeout(callPromise, timeoutMs ?? CALL_TIMEOUT_MS);
|
|
140
134
|
this.calls.delete(requestId);
|
|
@@ -143,16 +137,16 @@ class Popcorn {
|
|
|
143
137
|
/**
|
|
144
138
|
* Sends a message to an Elixir process (default or from options) and returns immediately.
|
|
145
139
|
*
|
|
146
|
-
* Unless passed via options, the name passed in `Popcorn.Wasm.
|
|
140
|
+
* Unless passed via options, the name passed in `Popcorn.Wasm.set_default_receiver/1` on the Elixir side is used.
|
|
147
141
|
* Throws "Unspecified target process" if default process is not set and no process is specified.
|
|
148
142
|
*/
|
|
149
143
|
cast(args, { process } = {}) {
|
|
150
144
|
this.assertStatus(["ready"]);
|
|
151
|
-
const targetProcess = process ?? this.
|
|
152
|
-
if (this.bridge === null)
|
|
153
|
-
throwError({ t: "unmounted" });
|
|
145
|
+
const targetProcess = process ?? this.defaultReceiver;
|
|
154
146
|
if (targetProcess === null)
|
|
155
147
|
throwError({ t: "bad_target" });
|
|
148
|
+
if (this.bridge === null)
|
|
149
|
+
throwError({ t: "unmounted" });
|
|
156
150
|
const requestId = this.requestId++;
|
|
157
151
|
this.trace("Main: cast: ", { requestId, process, args });
|
|
158
152
|
this.bridge.sendIframeRequest({
|
|
@@ -168,20 +162,27 @@ class Popcorn {
|
|
|
168
162
|
throwError({ t: "unmounted" });
|
|
169
163
|
this.trace("Main: deinit");
|
|
170
164
|
this.transition({ status: "deinit" });
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
165
|
+
this.teardownBridge("deinitialized");
|
|
166
|
+
this.logListeners.stdout.clear();
|
|
167
|
+
this.logListeners.stderr.clear();
|
|
168
|
+
this.messageHandlers.clear();
|
|
169
|
+
}
|
|
170
|
+
teardownBridge(errorCode) {
|
|
171
|
+
if (this.bridge) {
|
|
172
|
+
this.bridge.deinit();
|
|
173
|
+
this.bridge = null;
|
|
174
|
+
}
|
|
175
|
+
this.mountResolve = null;
|
|
176
|
+
this.defaultReceiver = null;
|
|
174
177
|
if (this.heartbeatTimeout) {
|
|
175
178
|
clearTimeout(this.heartbeatTimeout);
|
|
176
179
|
this.heartbeatTimeout = null;
|
|
177
180
|
}
|
|
178
|
-
this.logListeners.stdout.clear();
|
|
179
|
-
this.logListeners.stderr.clear();
|
|
180
181
|
for (const callData of this.calls.values()) {
|
|
181
182
|
const durationMs = performance.now() - callData.startTimeMs;
|
|
182
183
|
callData.resolve({
|
|
183
184
|
ok: false,
|
|
184
|
-
error: new PopcornError(
|
|
185
|
+
error: new PopcornError(errorCode),
|
|
185
186
|
durationMs,
|
|
186
187
|
});
|
|
187
188
|
}
|
|
@@ -204,14 +205,47 @@ class Popcorn {
|
|
|
204
205
|
listener(message);
|
|
205
206
|
});
|
|
206
207
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Registers a catch-all event handler. Returns an unsubscribe function.
|
|
210
|
+
*/
|
|
211
|
+
onMessage(handler) {
|
|
212
|
+
this.messageHandlers.add(handler);
|
|
213
|
+
return () => {
|
|
214
|
+
this.messageHandlers.delete(handler);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
onEvent({ eventName, payload }) {
|
|
218
|
+
if (eventName.startsWith("popcorn")) {
|
|
219
|
+
if (eventName === EVENT_NAMES.ELIXIR_READY) {
|
|
220
|
+
this.trace("Main: elixir VM ready");
|
|
221
|
+
}
|
|
222
|
+
else if (eventName === EVENT_NAMES.APP_READY) {
|
|
223
|
+
this.defaultReceiver = payload.name;
|
|
224
|
+
this.mountResolve?.();
|
|
225
|
+
this.mountResolve = null;
|
|
226
|
+
}
|
|
227
|
+
else if (eventName === EVENT_NAMES.SET_DEFAULT_RECEIVER) {
|
|
228
|
+
this.defaultReceiver = payload.name;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
this.trace("Unknown internal event:", eventName);
|
|
232
|
+
}
|
|
212
233
|
return;
|
|
213
234
|
}
|
|
214
|
-
|
|
235
|
+
this.messageHandlers.forEach((handler) => {
|
|
236
|
+
try {
|
|
237
|
+
handler(eventName, payload);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error(`Error in onMessage handler for '${eventName}':`, error);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
iframeHandler(data) {
|
|
245
|
+
if (data.type === MESSAGES.EVENT) {
|
|
246
|
+
this.onEvent(data.value);
|
|
247
|
+
}
|
|
248
|
+
else if (data.type === MESSAGES.STDOUT) {
|
|
215
249
|
this.notifyLogListeners("stdout", data.value);
|
|
216
250
|
}
|
|
217
251
|
else if (data.type === MESSAGES.STDERR) {
|
|
@@ -283,40 +317,10 @@ class Popcorn {
|
|
|
283
317
|
}
|
|
284
318
|
this.trace("Main: reloading iframe");
|
|
285
319
|
this.transition({ status: "reload" });
|
|
286
|
-
this.
|
|
287
|
-
this.bridge = null;
|
|
288
|
-
this.awaitedMessage = null;
|
|
289
|
-
if (this.heartbeatTimeout) {
|
|
290
|
-
clearTimeout(this.heartbeatTimeout);
|
|
291
|
-
this.heartbeatTimeout = null;
|
|
292
|
-
}
|
|
293
|
-
for (const callData of this.calls.values()) {
|
|
294
|
-
const durationMs = performance.now() - callData.startTimeMs;
|
|
295
|
-
callData.resolve({
|
|
296
|
-
ok: false,
|
|
297
|
-
error: new PopcornError("reload"),
|
|
298
|
-
durationMs,
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
this.calls.clear();
|
|
320
|
+
this.teardownBridge("reload");
|
|
302
321
|
this.onReloadCallback(reason);
|
|
303
322
|
this.mount();
|
|
304
323
|
}
|
|
305
|
-
awaitMessage(type) {
|
|
306
|
-
if (this.awaitedMessage) {
|
|
307
|
-
throwError({
|
|
308
|
-
t: "already_awaited",
|
|
309
|
-
messageType: this.awaitedMessage.type,
|
|
310
|
-
awaitedMessageType: type,
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
this.awaitedMessage = { type };
|
|
314
|
-
return new Promise((resolve) => {
|
|
315
|
-
if (!this.awaitedMessage)
|
|
316
|
-
throwError({ t: "assert" });
|
|
317
|
-
this.awaitedMessage.resolve = resolve;
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
324
|
trace(...messages) {
|
|
321
325
|
if (this.debug) {
|
|
322
326
|
console.debug(...messages);
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export declare const HEARTBEAT_TIMEOUT_MS = 60000;
|
|
|
4
4
|
export declare const HEARTBEAT_INTERVAL_MS = 500;
|
|
5
5
|
export declare const MAX_RELOAD_N = 3;
|
|
6
6
|
export type AnySerializable = any;
|
|
7
|
+
export type ElixirEvent = {
|
|
8
|
+
eventName: string;
|
|
9
|
+
payload: AnySerializable;
|
|
10
|
+
};
|
|
7
11
|
export type CallRequest = {
|
|
8
12
|
requestId: number;
|
|
9
13
|
process: string;
|
|
@@ -32,11 +36,8 @@ export type CallAck = {
|
|
|
32
36
|
};
|
|
33
37
|
/** Messages sent from iframe to parent window */
|
|
34
38
|
export type IframeResponse = {
|
|
35
|
-
type: "popcorn-
|
|
36
|
-
value:
|
|
37
|
-
} | {
|
|
38
|
-
type: "popcorn-startVm";
|
|
39
|
-
value: string;
|
|
39
|
+
type: "popcorn-event";
|
|
40
|
+
value: ElixirEvent;
|
|
40
41
|
} | {
|
|
41
42
|
type: "popcorn-call";
|
|
42
43
|
value: CallResponse;
|
|
@@ -59,8 +60,7 @@ export type IframeResponse = {
|
|
|
59
60
|
/** Union of all messages (requests and responses) */
|
|
60
61
|
export type Message = IframeRequest | IframeResponse;
|
|
61
62
|
export declare const MESSAGES: {
|
|
62
|
-
readonly
|
|
63
|
-
readonly START_VM: "popcorn-startVm";
|
|
63
|
+
readonly EVENT: "popcorn-event";
|
|
64
64
|
readonly CALL: "popcorn-call";
|
|
65
65
|
readonly CAST: "popcorn-cast";
|
|
66
66
|
readonly CALL_ACK: "popcorn-callAck";
|
|
@@ -69,4 +69,9 @@ export declare const MESSAGES: {
|
|
|
69
69
|
readonly HEARTBEAT: "popcorn-heartbeat";
|
|
70
70
|
readonly RELOAD: "popcorn-reload";
|
|
71
71
|
};
|
|
72
|
+
export declare const EVENT_NAMES: {
|
|
73
|
+
readonly ELIXIR_READY: "popcorn_elixir_ready";
|
|
74
|
+
readonly APP_READY: "popcorn_app_ready";
|
|
75
|
+
readonly SET_DEFAULT_RECEIVER: "popcorn_set_default_receiver";
|
|
76
|
+
};
|
|
72
77
|
export declare function isMessageType(type: string): type is Message["type"];
|
package/dist/types.mjs
CHANGED
|
@@ -3,8 +3,7 @@ const CALL_TIMEOUT_MS = 60_000;
|
|
|
3
3
|
const HEARTBEAT_TIMEOUT_MS = 60_000;
|
|
4
4
|
const MAX_RELOAD_N = 3;
|
|
5
5
|
const MESSAGES = {
|
|
6
|
-
|
|
7
|
-
START_VM: "popcorn-startVm",
|
|
6
|
+
EVENT: "popcorn-event",
|
|
8
7
|
CALL: "popcorn-call",
|
|
9
8
|
CAST: "popcorn-cast",
|
|
10
9
|
CALL_ACK: "popcorn-callAck",
|
|
@@ -13,9 +12,14 @@ const MESSAGES = {
|
|
|
13
12
|
HEARTBEAT: "popcorn-heartbeat",
|
|
14
13
|
RELOAD: "popcorn-reload",
|
|
15
14
|
};
|
|
15
|
+
const EVENT_NAMES = {
|
|
16
|
+
ELIXIR_READY: "popcorn_elixir_ready",
|
|
17
|
+
APP_READY: "popcorn_app_ready",
|
|
18
|
+
SET_DEFAULT_RECEIVER: "popcorn_set_default_receiver",
|
|
19
|
+
};
|
|
16
20
|
const MESSAGES_TYPES = new Set(Object.values(MESSAGES));
|
|
17
21
|
function isMessageType(type) {
|
|
18
22
|
return MESSAGES_TYPES.has(type);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
export { CALL_TIMEOUT_MS, HEARTBEAT_TIMEOUT_MS, INIT_VM_TIMEOUT_MS, MAX_RELOAD_N, MESSAGES, isMessageType };
|
|
25
|
+
export { CALL_TIMEOUT_MS, EVENT_NAMES, HEARTBEAT_TIMEOUT_MS, INIT_VM_TIMEOUT_MS, MAX_RELOAD_N, MESSAGES, isMessageType };
|