@crup/port 0.1.2 → 0.1.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/README.md +25 -7
- package/dist/child.d.ts +3 -2
- package/dist/child.mjs +34 -8
- package/dist/index.d.ts +4 -1
- package/dist/index.mjs +186 -77
- package/package.json +3 -1
- package/dist/child.mjs.map +0 -1
- package/dist/index.global.js +0 -365
- package/dist/index.global.js.map +0 -1
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@crup/port)
|
|
4
4
|
[](https://www.npmjs.com/package/@crup/port)
|
|
5
|
+
[](https://bundlejs.com/?q=%40crup%2Fport)
|
|
5
6
|
[](https://github.com/crup/port/blob/main/LICENSE)
|
|
6
7
|
[](https://github.com/crup/port/actions/workflows/ci.yml)
|
|
7
8
|
[](https://github.com/crup/port/actions/workflows/docs.yml)
|
|
8
9
|
|
|
9
10
|
Protocol-first iframe runtime for explicit host/child communication.
|
|
10
11
|
|
|
11
|
-
`@crup/port` exists for the part of embedded app work that usually rots first: lifecycle, handshake timing, and message discipline. It gives the host page a small runtime for mounting an iframe, opening it inline or in a modal,
|
|
12
|
+
`@crup/port` exists for the part of embedded app work that usually rots first: lifecycle, handshake timing, and message discipline. It gives the host page a small runtime for mounting an iframe, opening it inline or in a modal, pinning exact origins, and exchanging request/response/error messages without ad hoc `postMessage` glue.
|
|
12
13
|
|
|
13
14
|
Package: https://www.npmjs.com/package/@crup/port
|
|
14
15
|
|
|
@@ -85,6 +86,11 @@ const child = createChildPort({
|
|
|
85
86
|
child.on('request:system:ping', (message) => {
|
|
86
87
|
const request = message as { messageId: string; payload?: unknown };
|
|
87
88
|
|
|
89
|
+
if (!request.payload) {
|
|
90
|
+
child.reject(request.messageId, 'missing ping payload');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
88
94
|
child.respond(request.messageId, {
|
|
89
95
|
ok: true,
|
|
90
96
|
receivedAt: Date.now()
|
|
@@ -98,11 +104,11 @@ child.resize(document.body.scrollHeight);
|
|
|
98
104
|
## What You Get
|
|
99
105
|
|
|
100
106
|
- Explicit lifecycle: `idle -> mounting -> mounted -> handshaking -> ready -> open -> closed -> destroyed`
|
|
101
|
-
-
|
|
107
|
+
- Explicit origin pinning on both host and child
|
|
102
108
|
- Inline and modal host modes
|
|
103
|
-
- Event emission plus request/response RPC
|
|
109
|
+
- Event emission plus request/response/error RPC
|
|
104
110
|
- Child-driven height updates
|
|
105
|
-
- Small ESM-
|
|
111
|
+
- Small ESM-only bundle built with `tsup`
|
|
106
112
|
|
|
107
113
|
## API Surface
|
|
108
114
|
|
|
@@ -121,7 +127,7 @@ Host runtime with:
|
|
|
121
127
|
- `update(partialConfig)`
|
|
122
128
|
- `getState()`
|
|
123
129
|
|
|
124
|
-
### `createChildPort(config
|
|
130
|
+
### `createChildPort(config)`
|
|
125
131
|
|
|
126
132
|
Child runtime with:
|
|
127
133
|
|
|
@@ -129,6 +135,7 @@ Child runtime with:
|
|
|
129
135
|
- `emit(type, payload?)`
|
|
130
136
|
- `on(type, handler)`
|
|
131
137
|
- `respond(messageId, payload)`
|
|
138
|
+
- `reject(messageId, payload?)`
|
|
132
139
|
- `resize(height)`
|
|
133
140
|
- `destroy()`
|
|
134
141
|
|
|
@@ -167,6 +174,17 @@ type PortMessage = {
|
|
|
167
174
|
- Release process: [`docs/releasing.md`](docs/releasing.md)
|
|
168
175
|
- Contributing: [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
169
176
|
|
|
177
|
+
## Positioning
|
|
178
|
+
|
|
179
|
+
`@crup/port` stays intentionally narrow:
|
|
180
|
+
|
|
181
|
+
- it is a protocol runtime for iframe lifecycle, handshake, resize, and correlated messaging
|
|
182
|
+
- it is not a framework adapter layer for React, Vue, or Web Components
|
|
183
|
+
- it is not a generic method bridge that reaches into arbitrary child code
|
|
184
|
+
- it is not an automatic DOM sync system beyond explicit child-driven `resize()`
|
|
185
|
+
|
|
186
|
+
That scope is what keeps the package small and predictable.
|
|
187
|
+
|
|
170
188
|
## Local Development
|
|
171
189
|
|
|
172
190
|
```bash
|
|
@@ -189,12 +207,12 @@ Useful scripts:
|
|
|
189
207
|
|
|
190
208
|
- `ci.yml` validates lint, types, tests, package build, demo build, README checks, size output, and package packing.
|
|
191
209
|
- `docs.yml` deploys the Vite demo to GitHub Pages at `https://crup.github.io/port/`.
|
|
192
|
-
- `release.yml` is a guarded manual stable release workflow modeled on `crup/react-timer-hook`.
|
|
210
|
+
- `release.yml` is a guarded manual stable release workflow modeled on `crup/react-timer-hook`, and it persists the published version back to `main`.
|
|
193
211
|
- `prerelease.yml` publishes a manual alpha prerelease from the `next` branch.
|
|
194
212
|
|
|
195
213
|
## Security
|
|
196
214
|
|
|
197
|
-
This package helps
|
|
215
|
+
This package helps enforce the runtime boundary, but it cannot secure a weak embed strategy on its own. Always pin `allowedOrigin`, set restrictive iframe attributes, and validate application-level payloads. The practical guidance lives in [`docs/security.md`](docs/security.md).
|
|
198
216
|
|
|
199
217
|
## OSS Baseline
|
|
200
218
|
|
package/dist/child.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
|
|
2
2
|
interface ChildPortConfig {
|
|
3
|
-
allowedOrigin
|
|
3
|
+
allowedOrigin: string;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
interface ChildPort {
|
|
@@ -8,9 +8,10 @@ interface ChildPort {
|
|
|
8
8
|
emit(type: string, payload?: unknown): void;
|
|
9
9
|
on(type: string, handler: EventHandler): void;
|
|
10
10
|
respond(messageId: string, payload: unknown): void;
|
|
11
|
+
reject(messageId: string, payload?: unknown): void;
|
|
11
12
|
resize(height: number): void;
|
|
12
13
|
destroy(): void;
|
|
13
14
|
}
|
|
14
|
-
declare function createChildPort(config
|
|
15
|
+
declare function createChildPort(config: ChildPortConfig): ChildPort;
|
|
15
16
|
|
|
16
17
|
export { type ChildPort, createChildPort };
|
package/dist/child.mjs
CHANGED
|
@@ -16,7 +16,18 @@ var Emitter = class {
|
|
|
16
16
|
if (!list) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
for (const handler of list) {
|
|
20
|
+
await handler(payload);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/errors.ts
|
|
26
|
+
var PortError = class extends Error {
|
|
27
|
+
constructor(code, message) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.name = "PortError";
|
|
20
31
|
}
|
|
21
32
|
};
|
|
22
33
|
|
|
@@ -35,23 +46,23 @@ function isPortMessage(value) {
|
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
// src/child.ts
|
|
38
|
-
function createChildPort(config
|
|
49
|
+
function createChildPort(config) {
|
|
50
|
+
validateChildConfig(config);
|
|
39
51
|
const emitter = new Emitter();
|
|
40
52
|
let instanceId = null;
|
|
41
53
|
let hostWindow = null;
|
|
42
|
-
|
|
54
|
+
const hostOrigin = config.allowedOrigin;
|
|
43
55
|
const listener = (event) => {
|
|
44
56
|
if (!isPortMessage(event.data)) {
|
|
45
57
|
return;
|
|
46
58
|
}
|
|
47
59
|
const message = event.data;
|
|
48
60
|
if (message.kind === "system" && message.type === "port:hello") {
|
|
49
|
-
instanceId = message.instanceId;
|
|
50
|
-
hostWindow = event.source;
|
|
51
|
-
hostOrigin = hostOrigin ?? event.origin;
|
|
52
61
|
if (hostOrigin !== event.origin) {
|
|
53
62
|
return;
|
|
54
63
|
}
|
|
64
|
+
instanceId = message.instanceId;
|
|
65
|
+
hostWindow = event.source;
|
|
55
66
|
ready();
|
|
56
67
|
return;
|
|
57
68
|
}
|
|
@@ -97,6 +108,9 @@ function createChildPort(config = {}) {
|
|
|
97
108
|
function respond(messageId, payload) {
|
|
98
109
|
post({ kind: "response", type: "port:response", payload, replyTo: messageId });
|
|
99
110
|
}
|
|
111
|
+
function reject(messageId, payload = "Rejected") {
|
|
112
|
+
post({ kind: "error", type: "port:error", payload, replyTo: messageId });
|
|
113
|
+
}
|
|
100
114
|
function resize(height) {
|
|
101
115
|
if (!Number.isFinite(height) || height < 0) {
|
|
102
116
|
return;
|
|
@@ -106,9 +120,21 @@ function createChildPort(config = {}) {
|
|
|
106
120
|
function destroy() {
|
|
107
121
|
window.removeEventListener("message", listener);
|
|
108
122
|
}
|
|
109
|
-
return { ready, emit, on, respond, resize, destroy };
|
|
123
|
+
return { ready, emit, on, respond, reject, resize, destroy };
|
|
124
|
+
}
|
|
125
|
+
function validateChildConfig(config) {
|
|
126
|
+
if (!config.allowedOrigin) {
|
|
127
|
+
throw new PortError("INVALID_CONFIG", "allowedOrigin is required for child ports");
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const parsed = new URL(config.allowedOrigin);
|
|
131
|
+
if (parsed.origin !== config.allowedOrigin) {
|
|
132
|
+
throw new Error("Origin must not include a path");
|
|
133
|
+
}
|
|
134
|
+
} catch {
|
|
135
|
+
throw new PortError("INVALID_CONFIG", "allowedOrigin must be an exact origin such as https://host.example.com");
|
|
136
|
+
}
|
|
110
137
|
}
|
|
111
138
|
export {
|
|
112
139
|
createChildPort
|
|
113
140
|
};
|
|
114
|
-
//# sourceMappingURL=child.mjs.map
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,9 @@ interface PortConfig {
|
|
|
23
23
|
maxHeight?: number;
|
|
24
24
|
}
|
|
25
25
|
type EventHandler<T = unknown> = (payload: T) => void | Promise<void>;
|
|
26
|
+
interface ChildPortConfig {
|
|
27
|
+
allowedOrigin: string;
|
|
28
|
+
}
|
|
26
29
|
|
|
27
30
|
interface Port {
|
|
28
31
|
mount(): Promise<void>;
|
|
@@ -43,4 +46,4 @@ declare class PortError extends Error {
|
|
|
43
46
|
constructor(code: PortErrorCode, message: string);
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
export { type Port, type PortConfig, PortError, type PortErrorCode, type PortMessage, type PortState, createPort };
|
|
49
|
+
export { type ChildPortConfig, type Port, type PortConfig, PortError, type PortErrorCode, type PortMessage, type PortState, createPort };
|
package/dist/index.mjs
CHANGED
|
@@ -16,7 +16,9 @@ var Emitter = class {
|
|
|
16
16
|
if (!list) {
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
for (const handler of list) {
|
|
20
|
+
await handler(payload);
|
|
21
|
+
}
|
|
20
22
|
}
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -65,6 +67,9 @@ function createPort(input) {
|
|
|
65
67
|
let iframe = null;
|
|
66
68
|
let targetNode = null;
|
|
67
69
|
let modalRoot = null;
|
|
70
|
+
let pendingHandshake = null;
|
|
71
|
+
let modalBackdropListener = null;
|
|
72
|
+
let modalKeydownListener = null;
|
|
68
73
|
const listener = (event) => {
|
|
69
74
|
if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
|
|
70
75
|
return;
|
|
@@ -80,8 +85,12 @@ function createPort(input) {
|
|
|
80
85
|
return;
|
|
81
86
|
}
|
|
82
87
|
if (msg.kind === "system" && msg.type === "port:ready") {
|
|
83
|
-
if (state === "handshaking") {
|
|
88
|
+
if (state === "handshaking" && pendingHandshake) {
|
|
89
|
+
clearTimeout(pendingHandshake.timeout);
|
|
90
|
+
const { resolve } = pendingHandshake;
|
|
91
|
+
pendingHandshake = null;
|
|
84
92
|
state = "ready";
|
|
93
|
+
resolve();
|
|
85
94
|
}
|
|
86
95
|
return;
|
|
87
96
|
}
|
|
@@ -133,59 +142,68 @@ function createPort(input) {
|
|
|
133
142
|
async function mount() {
|
|
134
143
|
ensureState(["idle"], "mount");
|
|
135
144
|
state = "mounting";
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
145
|
+
try {
|
|
146
|
+
targetNode = resolveTarget(config.target);
|
|
147
|
+
iframe = document.createElement("iframe");
|
|
148
|
+
iframe.src = config.url;
|
|
149
|
+
iframe.style.width = "100%";
|
|
150
|
+
iframe.style.border = "0";
|
|
151
|
+
iframe.style.display = config.mode === "modal" ? "none" : "block";
|
|
152
|
+
if (config.mode === "modal") {
|
|
153
|
+
modalRoot = document.createElement("div");
|
|
154
|
+
modalRoot.style.position = "fixed";
|
|
155
|
+
modalRoot.style.inset = "0";
|
|
156
|
+
modalRoot.style.background = "rgba(0,0,0,0.5)";
|
|
157
|
+
modalRoot.style.display = "none";
|
|
158
|
+
modalRoot.style.alignItems = "center";
|
|
159
|
+
modalRoot.style.justifyContent = "center";
|
|
160
|
+
const container = document.createElement("div");
|
|
161
|
+
container.style.width = "min(900px, 95vw)";
|
|
162
|
+
container.style.height = "min(85vh, 900px)";
|
|
163
|
+
container.style.background = "#fff";
|
|
164
|
+
container.style.borderRadius = "8px";
|
|
165
|
+
container.style.overflow = "hidden";
|
|
166
|
+
iframe.style.display = "block";
|
|
167
|
+
iframe.style.height = "100%";
|
|
168
|
+
container.append(iframe);
|
|
169
|
+
modalRoot.append(container);
|
|
170
|
+
targetNode.append(modalRoot);
|
|
171
|
+
modalBackdropListener = (event) => {
|
|
172
|
+
if (event.target === modalRoot) {
|
|
173
|
+
void close();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
modalRoot.addEventListener("click", modalBackdropListener);
|
|
177
|
+
modalKeydownListener = (event) => {
|
|
178
|
+
if (event.key === "Escape" && state === "open") {
|
|
179
|
+
void close();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
window.addEventListener("keydown", modalKeydownListener);
|
|
183
|
+
} else {
|
|
184
|
+
targetNode.append(iframe);
|
|
185
|
+
}
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
const timer = setTimeout(() => {
|
|
188
|
+
reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
|
|
189
|
+
}, config.iframeLoadTimeoutMs);
|
|
190
|
+
iframe?.addEventListener(
|
|
191
|
+
"load",
|
|
192
|
+
() => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
resolve();
|
|
195
|
+
},
|
|
196
|
+
{ once: true }
|
|
197
|
+
);
|
|
170
198
|
});
|
|
171
|
-
|
|
172
|
-
|
|
199
|
+
state = "mounted";
|
|
200
|
+
await handshake();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
cleanupPendingHandshake();
|
|
203
|
+
cleanupMountedElements();
|
|
204
|
+
state = "idle";
|
|
205
|
+
throw error;
|
|
173
206
|
}
|
|
174
|
-
await new Promise((resolve, reject) => {
|
|
175
|
-
const timer = setTimeout(() => {
|
|
176
|
-
reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
|
|
177
|
-
}, config.iframeLoadTimeoutMs);
|
|
178
|
-
iframe?.addEventListener(
|
|
179
|
-
"load",
|
|
180
|
-
() => {
|
|
181
|
-
clearTimeout(timer);
|
|
182
|
-
resolve();
|
|
183
|
-
},
|
|
184
|
-
{ once: true }
|
|
185
|
-
);
|
|
186
|
-
});
|
|
187
|
-
state = "mounted";
|
|
188
|
-
await handshake();
|
|
189
207
|
}
|
|
190
208
|
async function handshake() {
|
|
191
209
|
ensureState(["mounted"], "handshake");
|
|
@@ -193,19 +211,25 @@ function createPort(input) {
|
|
|
193
211
|
throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
|
|
194
212
|
}
|
|
195
213
|
state = "handshaking";
|
|
196
|
-
post({ kind: "system", type: "port:hello" });
|
|
197
214
|
await new Promise((resolve, reject) => {
|
|
198
215
|
const timer = setTimeout(() => {
|
|
199
|
-
|
|
216
|
+
pendingHandshake = null;
|
|
200
217
|
reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
|
|
201
218
|
}, config.handshakeTimeoutMs);
|
|
202
|
-
|
|
203
|
-
|
|
219
|
+
pendingHandshake = {
|
|
220
|
+
resolve,
|
|
221
|
+
reject,
|
|
222
|
+
timeout: timer
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
post({ kind: "system", type: "port:hello" });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (pendingHandshake) {
|
|
228
|
+
pendingHandshake = null;
|
|
204
229
|
clearTimeout(timer);
|
|
205
|
-
clearInterval(poll);
|
|
206
|
-
resolve();
|
|
207
230
|
}
|
|
208
|
-
|
|
231
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
232
|
+
}
|
|
209
233
|
});
|
|
210
234
|
if (config.mode === "inline") {
|
|
211
235
|
state = "open";
|
|
@@ -241,12 +265,9 @@ function createPort(input) {
|
|
|
241
265
|
entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
|
|
242
266
|
});
|
|
243
267
|
pending.clear();
|
|
268
|
+
cleanupPendingHandshake(new PortError("PORT_DESTROYED", "Port has been destroyed"));
|
|
244
269
|
window.removeEventListener("message", listener);
|
|
245
|
-
|
|
246
|
-
modalRoot?.remove();
|
|
247
|
-
iframe = null;
|
|
248
|
-
modalRoot = null;
|
|
249
|
-
targetNode = null;
|
|
270
|
+
cleanupMountedElements();
|
|
250
271
|
state = "destroyed";
|
|
251
272
|
}
|
|
252
273
|
function post(message) {
|
|
@@ -257,7 +278,7 @@ function createPort(input) {
|
|
|
257
278
|
protocol: PROTOCOL,
|
|
258
279
|
version: VERSION,
|
|
259
280
|
instanceId,
|
|
260
|
-
messageId: randomId(),
|
|
281
|
+
messageId: message.messageId ?? randomId(),
|
|
261
282
|
...message
|
|
262
283
|
};
|
|
263
284
|
iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
|
|
@@ -275,22 +296,19 @@ function createPort(input) {
|
|
|
275
296
|
}
|
|
276
297
|
ensureState(["ready", "open", "closed"], "call");
|
|
277
298
|
const messageId = randomId();
|
|
278
|
-
const message = {
|
|
279
|
-
protocol: PROTOCOL,
|
|
280
|
-
version: VERSION,
|
|
281
|
-
instanceId,
|
|
282
|
-
messageId,
|
|
283
|
-
kind: "request",
|
|
284
|
-
type,
|
|
285
|
-
payload
|
|
286
|
-
};
|
|
287
|
-
iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
|
|
288
299
|
return new Promise((resolve, reject) => {
|
|
289
300
|
const timeout = setTimeout(() => {
|
|
290
301
|
pending.delete(messageId);
|
|
291
302
|
reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
|
|
292
303
|
}, config.callTimeoutMs);
|
|
293
304
|
pending.set(messageId, { resolve, reject, timeout });
|
|
305
|
+
try {
|
|
306
|
+
post({ kind: "request", type, payload, messageId });
|
|
307
|
+
} catch (error) {
|
|
308
|
+
clearTimeout(timeout);
|
|
309
|
+
pending.delete(messageId);
|
|
310
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
311
|
+
}
|
|
294
312
|
});
|
|
295
313
|
}
|
|
296
314
|
function on(type, handler) {
|
|
@@ -300,6 +318,14 @@ function createPort(input) {
|
|
|
300
318
|
emitter.off(type, handler);
|
|
301
319
|
}
|
|
302
320
|
function update(next) {
|
|
321
|
+
validatePartialConfig(next, config);
|
|
322
|
+
if (state !== "idle") {
|
|
323
|
+
for (const key of ["url", "allowedOrigin", "target", "mode"]) {
|
|
324
|
+
if (key in next) {
|
|
325
|
+
throw new PortError("INVALID_STATE", `${key} cannot be updated after mount`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
303
329
|
Object.assign(config, next);
|
|
304
330
|
}
|
|
305
331
|
function getState() {
|
|
@@ -315,6 +341,34 @@ function createPort(input) {
|
|
|
315
341
|
const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
|
|
316
342
|
iframe.style.height = `${bounded}px`;
|
|
317
343
|
}
|
|
344
|
+
function cleanupMountedElements() {
|
|
345
|
+
cleanupModalListeners();
|
|
346
|
+
iframe?.remove();
|
|
347
|
+
modalRoot?.remove();
|
|
348
|
+
iframe = null;
|
|
349
|
+
modalRoot = null;
|
|
350
|
+
targetNode = null;
|
|
351
|
+
}
|
|
352
|
+
function cleanupModalListeners() {
|
|
353
|
+
if (modalRoot && modalBackdropListener) {
|
|
354
|
+
modalRoot.removeEventListener("click", modalBackdropListener);
|
|
355
|
+
}
|
|
356
|
+
if (modalKeydownListener) {
|
|
357
|
+
window.removeEventListener("keydown", modalKeydownListener);
|
|
358
|
+
}
|
|
359
|
+
modalBackdropListener = null;
|
|
360
|
+
modalKeydownListener = null;
|
|
361
|
+
}
|
|
362
|
+
function cleanupPendingHandshake(reason) {
|
|
363
|
+
if (!pendingHandshake) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
clearTimeout(pendingHandshake.timeout);
|
|
367
|
+
if (reason !== void 0) {
|
|
368
|
+
pendingHandshake.reject(reason);
|
|
369
|
+
}
|
|
370
|
+
pendingHandshake = null;
|
|
371
|
+
}
|
|
318
372
|
return {
|
|
319
373
|
mount,
|
|
320
374
|
open,
|
|
@@ -332,9 +386,64 @@ function validateConfig(config) {
|
|
|
332
386
|
if (!config.url || !config.allowedOrigin || !config.target) {
|
|
333
387
|
throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
|
|
334
388
|
}
|
|
389
|
+
validateUrl(config.url);
|
|
390
|
+
validateOrigin(config.allowedOrigin);
|
|
391
|
+
validatePartialConfig(config, {
|
|
392
|
+
minHeight: config.minHeight ?? 0,
|
|
393
|
+
maxHeight: config.maxHeight ?? Number.MAX_SAFE_INTEGER
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function validatePartialConfig(next, current) {
|
|
397
|
+
if (next.mode && next.mode !== "inline" && next.mode !== "modal") {
|
|
398
|
+
throw new PortError("INVALID_CONFIG", "mode must be either inline or modal");
|
|
399
|
+
}
|
|
400
|
+
if (typeof next.url === "string") {
|
|
401
|
+
validateUrl(next.url);
|
|
402
|
+
}
|
|
403
|
+
if (typeof next.allowedOrigin === "string") {
|
|
404
|
+
validateOrigin(next.allowedOrigin);
|
|
405
|
+
}
|
|
406
|
+
for (const [key, value] of [
|
|
407
|
+
["handshakeTimeoutMs", next.handshakeTimeoutMs],
|
|
408
|
+
["callTimeoutMs", next.callTimeoutMs],
|
|
409
|
+
["iframeLoadTimeoutMs", next.iframeLoadTimeoutMs]
|
|
410
|
+
]) {
|
|
411
|
+
if (value !== void 0 && (!Number.isFinite(value) || value <= 0)) {
|
|
412
|
+
throw new PortError("INVALID_CONFIG", `${key} must be a positive number`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const [key, value] of [
|
|
416
|
+
["minHeight", next.minHeight],
|
|
417
|
+
["maxHeight", next.maxHeight]
|
|
418
|
+
]) {
|
|
419
|
+
if (value !== void 0 && (!Number.isFinite(value) || value < 0)) {
|
|
420
|
+
throw new PortError("INVALID_CONFIG", `${key} must be a non-negative number`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const minHeight = next.minHeight ?? current.minHeight;
|
|
424
|
+
const maxHeight = next.maxHeight ?? current.maxHeight;
|
|
425
|
+
if (minHeight > maxHeight) {
|
|
426
|
+
throw new PortError("INVALID_CONFIG", "minHeight cannot be greater than maxHeight");
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function validateOrigin(origin) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = new URL(origin);
|
|
432
|
+
if (parsed.origin !== origin) {
|
|
433
|
+
throw new Error("Origin must not include a path");
|
|
434
|
+
}
|
|
435
|
+
} catch {
|
|
436
|
+
throw new PortError("INVALID_CONFIG", "allowedOrigin must be an exact origin such as https://child.example.com");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function validateUrl(url) {
|
|
440
|
+
try {
|
|
441
|
+
void new URL(url, window.location.href);
|
|
442
|
+
} catch {
|
|
443
|
+
throw new PortError("INVALID_CONFIG", "url must be a valid iframe URL");
|
|
444
|
+
}
|
|
335
445
|
}
|
|
336
446
|
export {
|
|
337
447
|
PortError,
|
|
338
448
|
createPort
|
|
339
449
|
};
|
|
340
|
-
//# sourceMappingURL=index.mjs.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crup/port",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A lightweight protocol-first iframe runtime for building secure host/child embeds with explicit lifecycle and messaging.",
|
|
5
5
|
"homepage": "https://crup.github.io/port/",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"dist",
|
|
47
|
+
"!dist/**/*.map",
|
|
48
|
+
"!dist/**/*.global.js",
|
|
47
49
|
"README.md",
|
|
48
50
|
"LICENSE"
|
|
49
51
|
],
|
package/dist/child.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/emitter.ts","../src/utils.ts","../src/child.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport type { ChildPortConfig, EventHandler, PortMessage } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\nexport interface ChildPort {\n ready(): void;\n emit(type: string, payload?: unknown): void;\n on(type: string, handler: EventHandler): void;\n respond(messageId: string, payload: unknown): void;\n resize(height: number): void;\n destroy(): void;\n}\n\nexport function createChildPort(config: ChildPortConfig = {}): ChildPort {\n const emitter = new Emitter();\n let instanceId: string | null = null;\n let hostWindow: Window | null = null;\n let hostOrigin: string | null = config.allowedOrigin ?? null;\n\n const listener = (event: MessageEvent): void => {\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const message = event.data;\n\n if (message.kind === 'system' && message.type === 'port:hello') {\n instanceId = message.instanceId;\n hostWindow = event.source as Window;\n hostOrigin = hostOrigin ?? event.origin;\n\n if (hostOrigin !== event.origin) {\n return;\n }\n\n ready();\n return;\n }\n\n if (!instanceId || !hostWindow || message.instanceId !== instanceId) {\n return;\n }\n\n if (event.source !== hostWindow || event.origin !== hostOrigin) {\n return;\n }\n\n if (message.kind === 'event') {\n void emitter.emit(message.type, message.payload);\n return;\n }\n\n if (message.kind === 'request') {\n void emitter.emit(`request:${message.type}`, message);\n }\n };\n\n window.addEventListener('message', listener);\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!instanceId || !hostWindow || !hostOrigin) {\n return;\n }\n\n hostWindow.postMessage(\n {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n } satisfies PortMessage,\n hostOrigin\n );\n }\n\n function ready(): void {\n post({ kind: 'system', type: 'port:ready' });\n }\n\n function emit(type: string, payload?: unknown): void {\n post({ kind: 'event', type, payload });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function respond(messageId: string, payload: unknown): void {\n post({ kind: 'response', type: 'port:response', payload, replyTo: messageId });\n }\n\n function resize(height: number): void {\n if (!Number.isFinite(height) || height < 0) {\n return;\n }\n post({ kind: 'event', type: 'port:resize', payload: height });\n }\n\n function destroy(): void {\n window.removeEventListener('message', listener);\n }\n\n return { ready, emit, on, respond, resize, destroy };\n}\n"],"mappings":";AAEO,IAAM,UAAN,MAAc;AAAA,EAAd;AACL,SAAQ,YAAY,oBAAI,IAA+B;AAAA;AAAA,EAEvD,GAAG,MAAc,SAA6B;AAC5C,UAAM,MAAM,KAAK,UAAU,IAAI,IAAI,KAAK,oBAAI,IAAkB;AAC9D,QAAI,IAAI,OAAO;AACf,SAAK,UAAU,IAAI,MAAM,GAAG;AAAA,EAC9B;AAAA,EAEA,IAAI,MAAc,SAA6B;AAC7C,SAAK,UAAU,IAAI,IAAI,GAAG,OAAO,OAAO;AAAA,EAC1C;AAAA,EAEA,MAAM,KAAK,MAAc,SAAiC;AACxD,UAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AACpC,QAAI,CAAC,MAAM;AACT;AAAA,IACF;AACA,UAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,OAAO,YAAY,QAAQ,OAAO,CAAC,CAAC;AAAA,EACtE;AACF;;;ACpBO,IAAM,WAAW;AACjB,IAAM,UAAU;AAEhB,SAAS,SAAS,SAAS,OAAe;AAC/C,SAAO,GAAG,MAAM,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC7D;AAEO,SAAS,cAAc,OAAsC;AAClE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO;AACb,SACE,KAAK,aAAa,YAClB,KAAK,YAAY,WACjB,OAAO,KAAK,eAAe,YAC3B,OAAO,KAAK,cAAc,YAC1B,OAAO,KAAK,SAAS,YACrB,OAAO,KAAK,SAAS;AAEzB;;;ACVO,SAAS,gBAAgB,SAA0B,CAAC,GAAc;AACvE,QAAM,UAAU,IAAI,QAAQ;AAC5B,MAAI,aAA4B;AAChC,MAAI,aAA4B;AAChC,MAAI,aAA4B,OAAO,iBAAiB;AAExD,QAAM,WAAW,CAAC,UAA8B;AAC9C,QAAI,CAAC,cAAc,MAAM,IAAI,GAAG;AAC9B;AAAA,IACF;AAEA,UAAM,UAAU,MAAM;AAEtB,QAAI,QAAQ,SAAS,YAAY,QAAQ,SAAS,cAAc;AAC9D,mBAAa,QAAQ;AACrB,mBAAa,MAAM;AACnB,mBAAa,cAAc,MAAM;AAEjC,UAAI,eAAe,MAAM,QAAQ;AAC/B;AAAA,MACF;AAEA,YAAM;AACN;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,CAAC,cAAc,QAAQ,eAAe,YAAY;AACnE;AAAA,IACF;AAEA,QAAI,MAAM,WAAW,cAAc,MAAM,WAAW,YAAY;AAC9D;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,SAAS;AAC5B,WAAK,QAAQ,KAAK,QAAQ,MAAM,QAAQ,OAAO;AAC/C;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,WAAW;AAC9B,WAAK,QAAQ,KAAK,WAAW,QAAQ,IAAI,IAAI,OAAO;AAAA,IACtD;AAAA,EACF;AAEA,SAAO,iBAAiB,WAAW,QAAQ;AAE3C,WAAS,KAAK,SAA2E;AACvF,QAAI,CAAC,cAAc,CAAC,cAAc,CAAC,YAAY;AAC7C;AAAA,IACF;AAEA,eAAW;AAAA,MACT;AAAA,QACE,UAAU;AAAA,QACV,SAAS;AAAA,QACT;AAAA,QACA,WAAW,SAAS;AAAA,QACpB,GAAG;AAAA,MACL;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,QAAc;AACrB,SAAK,EAAE,MAAM,UAAU,MAAM,aAAa,CAAC;AAAA,EAC7C;AAEA,WAAS,KAAK,MAAc,SAAyB;AACnD,SAAK,EAAE,MAAM,SAAS,MAAM,QAAQ,CAAC;AAAA,EACvC;AAEA,WAAS,GAAG,MAAc,SAA6B;AACrD,YAAQ,GAAG,MAAM,OAAO;AAAA,EAC1B;AAEA,WAAS,QAAQ,WAAmB,SAAwB;AAC1D,SAAK,EAAE,MAAM,YAAY,MAAM,iBAAiB,SAAS,SAAS,UAAU,CAAC;AAAA,EAC/E;AAEA,WAAS,OAAO,QAAsB;AACpC,QAAI,CAAC,OAAO,SAAS,MAAM,KAAK,SAAS,GAAG;AAC1C;AAAA,IACF;AACA,SAAK,EAAE,MAAM,SAAS,MAAM,eAAe,SAAS,OAAO,CAAC;AAAA,EAC9D;AAEA,WAAS,UAAgB;AACvB,WAAO,oBAAoB,WAAW,QAAQ;AAAA,EAChD;AAEA,SAAO,EAAE,OAAO,MAAM,IAAI,SAAS,QAAQ,QAAQ;AACrD;","names":[]}
|
package/dist/index.global.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var CrupPort = (() => {
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __export = (target, all) => {
|
|
8
|
-
for (var name in all)
|
|
9
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
-
};
|
|
11
|
-
var __copyProps = (to, from, except, desc) => {
|
|
12
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
-
for (let key of __getOwnPropNames(from))
|
|
14
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
-
|
|
21
|
-
// src/index.ts
|
|
22
|
-
var src_exports = {};
|
|
23
|
-
__export(src_exports, {
|
|
24
|
-
PortError: () => PortError,
|
|
25
|
-
createPort: () => createPort
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// src/emitter.ts
|
|
29
|
-
var Emitter = class {
|
|
30
|
-
constructor() {
|
|
31
|
-
this.listeners = /* @__PURE__ */ new Map();
|
|
32
|
-
}
|
|
33
|
-
on(type, handler) {
|
|
34
|
-
const set = this.listeners.get(type) ?? /* @__PURE__ */ new Set();
|
|
35
|
-
set.add(handler);
|
|
36
|
-
this.listeners.set(type, set);
|
|
37
|
-
}
|
|
38
|
-
off(type, handler) {
|
|
39
|
-
this.listeners.get(type)?.delete(handler);
|
|
40
|
-
}
|
|
41
|
-
async emit(type, payload) {
|
|
42
|
-
const list = this.listeners.get(type);
|
|
43
|
-
if (!list) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
await Promise.all([...list].map(async (handler) => handler(payload)));
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// src/errors.ts
|
|
51
|
-
var PortError = class extends Error {
|
|
52
|
-
constructor(code, message) {
|
|
53
|
-
super(message);
|
|
54
|
-
this.code = code;
|
|
55
|
-
this.name = "PortError";
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// src/utils.ts
|
|
60
|
-
var PROTOCOL = "crup.port";
|
|
61
|
-
var VERSION = "1";
|
|
62
|
-
function randomId(prefix = "msg") {
|
|
63
|
-
return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;
|
|
64
|
-
}
|
|
65
|
-
function isPortMessage(value) {
|
|
66
|
-
if (typeof value !== "object" || value === null) {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
const data = value;
|
|
70
|
-
return data.protocol === PROTOCOL && data.version === VERSION && typeof data.instanceId === "string" && typeof data.messageId === "string" && typeof data.kind === "string" && typeof data.type === "string";
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// src/host.ts
|
|
74
|
-
var DEFAULT_HANDSHAKE_TIMEOUT = 8e3;
|
|
75
|
-
var DEFAULT_CALL_TIMEOUT = 8e3;
|
|
76
|
-
var DEFAULT_IFRAME_LOAD_TIMEOUT = 8e3;
|
|
77
|
-
function createPort(input) {
|
|
78
|
-
validateConfig(input);
|
|
79
|
-
const config = {
|
|
80
|
-
...input,
|
|
81
|
-
mode: input.mode ?? "inline",
|
|
82
|
-
handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,
|
|
83
|
-
callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,
|
|
84
|
-
iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,
|
|
85
|
-
minHeight: input.minHeight ?? 0,
|
|
86
|
-
maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER
|
|
87
|
-
};
|
|
88
|
-
const instanceId = randomId("port");
|
|
89
|
-
const emitter = new Emitter();
|
|
90
|
-
const pending = /* @__PURE__ */ new Map();
|
|
91
|
-
let state = "idle";
|
|
92
|
-
let iframe = null;
|
|
93
|
-
let targetNode = null;
|
|
94
|
-
let modalRoot = null;
|
|
95
|
-
const listener = (event) => {
|
|
96
|
-
if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
if (event.origin !== config.allowedOrigin) {
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
if (!isPortMessage(event.data)) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
const msg = event.data;
|
|
106
|
-
if (msg.instanceId !== instanceId) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (msg.kind === "system" && msg.type === "port:ready") {
|
|
110
|
-
if (state === "handshaking") {
|
|
111
|
-
state = "ready";
|
|
112
|
-
}
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (msg.kind === "response" && msg.replyTo) {
|
|
116
|
-
const call2 = pending.get(msg.replyTo);
|
|
117
|
-
if (!call2) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
clearTimeout(call2.timeout);
|
|
121
|
-
pending.delete(msg.replyTo);
|
|
122
|
-
call2.resolve(msg.payload);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
if (msg.kind === "error" && msg.replyTo) {
|
|
126
|
-
const call2 = pending.get(msg.replyTo);
|
|
127
|
-
if (!call2) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
clearTimeout(call2.timeout);
|
|
131
|
-
pending.delete(msg.replyTo);
|
|
132
|
-
const reason = typeof msg.payload === "string" ? msg.payload : "Rejected";
|
|
133
|
-
call2.reject(new PortError("MESSAGE_REJECTED", reason));
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (msg.kind === "event" && msg.type === "port:resize") {
|
|
137
|
-
applyResize(msg.payload);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (msg.kind === "event") {
|
|
141
|
-
void emitter.emit(msg.type, msg.payload);
|
|
142
|
-
}
|
|
143
|
-
};
|
|
144
|
-
window.addEventListener("message", listener);
|
|
145
|
-
function ensureState(valid, nextAction) {
|
|
146
|
-
if (!valid.includes(state)) {
|
|
147
|
-
throw new PortError("INVALID_STATE", `Cannot ${nextAction} from state ${state}`);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
function resolveTarget(target) {
|
|
151
|
-
if (typeof target === "string") {
|
|
152
|
-
const node = document.querySelector(target);
|
|
153
|
-
if (!node) {
|
|
154
|
-
throw new PortError("INVALID_CONFIG", `Target ${target} was not found`);
|
|
155
|
-
}
|
|
156
|
-
return node;
|
|
157
|
-
}
|
|
158
|
-
return target;
|
|
159
|
-
}
|
|
160
|
-
async function mount() {
|
|
161
|
-
ensureState(["idle"], "mount");
|
|
162
|
-
state = "mounting";
|
|
163
|
-
targetNode = resolveTarget(config.target);
|
|
164
|
-
iframe = document.createElement("iframe");
|
|
165
|
-
iframe.src = config.url;
|
|
166
|
-
iframe.style.width = "100%";
|
|
167
|
-
iframe.style.border = "0";
|
|
168
|
-
iframe.style.display = config.mode === "modal" ? "none" : "block";
|
|
169
|
-
if (config.mode === "modal") {
|
|
170
|
-
modalRoot = document.createElement("div");
|
|
171
|
-
modalRoot.style.position = "fixed";
|
|
172
|
-
modalRoot.style.inset = "0";
|
|
173
|
-
modalRoot.style.background = "rgba(0,0,0,0.5)";
|
|
174
|
-
modalRoot.style.display = "none";
|
|
175
|
-
modalRoot.style.alignItems = "center";
|
|
176
|
-
modalRoot.style.justifyContent = "center";
|
|
177
|
-
const container = document.createElement("div");
|
|
178
|
-
container.style.width = "min(900px, 95vw)";
|
|
179
|
-
container.style.height = "min(85vh, 900px)";
|
|
180
|
-
container.style.background = "#fff";
|
|
181
|
-
container.style.borderRadius = "8px";
|
|
182
|
-
container.style.overflow = "hidden";
|
|
183
|
-
iframe.style.display = "block";
|
|
184
|
-
iframe.style.height = "100%";
|
|
185
|
-
container.append(iframe);
|
|
186
|
-
modalRoot.append(container);
|
|
187
|
-
targetNode.append(modalRoot);
|
|
188
|
-
modalRoot.addEventListener("click", (event) => {
|
|
189
|
-
if (event.target === modalRoot) {
|
|
190
|
-
void close();
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
window.addEventListener("keydown", (event) => {
|
|
194
|
-
if (event.key === "Escape" && state === "open") {
|
|
195
|
-
void close();
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
} else {
|
|
199
|
-
targetNode.append(iframe);
|
|
200
|
-
}
|
|
201
|
-
await new Promise((resolve, reject) => {
|
|
202
|
-
const timer = setTimeout(() => {
|
|
203
|
-
reject(new PortError("IFRAME_LOAD_TIMEOUT", "iframe did not load in time"));
|
|
204
|
-
}, config.iframeLoadTimeoutMs);
|
|
205
|
-
iframe?.addEventListener(
|
|
206
|
-
"load",
|
|
207
|
-
() => {
|
|
208
|
-
clearTimeout(timer);
|
|
209
|
-
resolve();
|
|
210
|
-
},
|
|
211
|
-
{ once: true }
|
|
212
|
-
);
|
|
213
|
-
});
|
|
214
|
-
state = "mounted";
|
|
215
|
-
await handshake();
|
|
216
|
-
}
|
|
217
|
-
async function handshake() {
|
|
218
|
-
ensureState(["mounted"], "handshake");
|
|
219
|
-
if (!iframe?.contentWindow) {
|
|
220
|
-
throw new PortError("INVALID_STATE", "iframe is unavailable for handshake");
|
|
221
|
-
}
|
|
222
|
-
state = "handshaking";
|
|
223
|
-
post({ kind: "system", type: "port:hello" });
|
|
224
|
-
await new Promise((resolve, reject) => {
|
|
225
|
-
const timer = setTimeout(() => {
|
|
226
|
-
clearInterval(poll);
|
|
227
|
-
reject(new PortError("HANDSHAKE_TIMEOUT", "handshake timed out"));
|
|
228
|
-
}, config.handshakeTimeoutMs);
|
|
229
|
-
const poll = setInterval(() => {
|
|
230
|
-
if (state === "ready") {
|
|
231
|
-
clearTimeout(timer);
|
|
232
|
-
clearInterval(poll);
|
|
233
|
-
resolve();
|
|
234
|
-
}
|
|
235
|
-
}, 10);
|
|
236
|
-
});
|
|
237
|
-
if (config.mode === "inline") {
|
|
238
|
-
state = "open";
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
function open() {
|
|
242
|
-
ensureState(["ready", "closed"], "open");
|
|
243
|
-
if (config.mode !== "modal") {
|
|
244
|
-
state = "open";
|
|
245
|
-
return Promise.resolve();
|
|
246
|
-
}
|
|
247
|
-
if (!modalRoot) {
|
|
248
|
-
throw new PortError("INVALID_STATE", "modal root missing");
|
|
249
|
-
}
|
|
250
|
-
modalRoot.style.display = "flex";
|
|
251
|
-
state = "open";
|
|
252
|
-
return Promise.resolve();
|
|
253
|
-
}
|
|
254
|
-
function close() {
|
|
255
|
-
ensureState(["open"], "close");
|
|
256
|
-
if (config.mode === "modal" && modalRoot) {
|
|
257
|
-
modalRoot.style.display = "none";
|
|
258
|
-
}
|
|
259
|
-
state = "closed";
|
|
260
|
-
return Promise.resolve();
|
|
261
|
-
}
|
|
262
|
-
function destroy() {
|
|
263
|
-
if (state === "destroyed") {
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
pending.forEach((entry) => {
|
|
267
|
-
clearTimeout(entry.timeout);
|
|
268
|
-
entry.reject(new PortError("PORT_DESTROYED", "Port has been destroyed"));
|
|
269
|
-
});
|
|
270
|
-
pending.clear();
|
|
271
|
-
window.removeEventListener("message", listener);
|
|
272
|
-
iframe?.remove();
|
|
273
|
-
modalRoot?.remove();
|
|
274
|
-
iframe = null;
|
|
275
|
-
modalRoot = null;
|
|
276
|
-
targetNode = null;
|
|
277
|
-
state = "destroyed";
|
|
278
|
-
}
|
|
279
|
-
function post(message) {
|
|
280
|
-
if (!iframe?.contentWindow) {
|
|
281
|
-
throw new PortError("INVALID_STATE", "iframe is not available");
|
|
282
|
-
}
|
|
283
|
-
const finalMessage = {
|
|
284
|
-
protocol: PROTOCOL,
|
|
285
|
-
version: VERSION,
|
|
286
|
-
instanceId,
|
|
287
|
-
messageId: randomId(),
|
|
288
|
-
...message
|
|
289
|
-
};
|
|
290
|
-
iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);
|
|
291
|
-
}
|
|
292
|
-
function send(type, payload) {
|
|
293
|
-
if (state === "destroyed") {
|
|
294
|
-
throw new PortError("PORT_DESTROYED", "Port is destroyed");
|
|
295
|
-
}
|
|
296
|
-
ensureState(["ready", "open", "closed"], "send");
|
|
297
|
-
post({ kind: "event", type, payload });
|
|
298
|
-
}
|
|
299
|
-
function call(type, payload) {
|
|
300
|
-
if (state === "destroyed") {
|
|
301
|
-
return Promise.reject(new PortError("PORT_DESTROYED", "Port is destroyed"));
|
|
302
|
-
}
|
|
303
|
-
ensureState(["ready", "open", "closed"], "call");
|
|
304
|
-
const messageId = randomId();
|
|
305
|
-
const message = {
|
|
306
|
-
protocol: PROTOCOL,
|
|
307
|
-
version: VERSION,
|
|
308
|
-
instanceId,
|
|
309
|
-
messageId,
|
|
310
|
-
kind: "request",
|
|
311
|
-
type,
|
|
312
|
-
payload
|
|
313
|
-
};
|
|
314
|
-
iframe?.contentWindow?.postMessage(message, config.allowedOrigin);
|
|
315
|
-
return new Promise((resolve, reject) => {
|
|
316
|
-
const timeout = setTimeout(() => {
|
|
317
|
-
pending.delete(messageId);
|
|
318
|
-
reject(new PortError("CALL_TIMEOUT", `${type} timed out`));
|
|
319
|
-
}, config.callTimeoutMs);
|
|
320
|
-
pending.set(messageId, { resolve, reject, timeout });
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
function on(type, handler) {
|
|
324
|
-
emitter.on(type, handler);
|
|
325
|
-
}
|
|
326
|
-
function off(type, handler) {
|
|
327
|
-
emitter.off(type, handler);
|
|
328
|
-
}
|
|
329
|
-
function update(next) {
|
|
330
|
-
Object.assign(config, next);
|
|
331
|
-
}
|
|
332
|
-
function getState() {
|
|
333
|
-
return state;
|
|
334
|
-
}
|
|
335
|
-
function applyResize(payload) {
|
|
336
|
-
if (!iframe) {
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
if (typeof payload !== "number" || Number.isNaN(payload)) {
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));
|
|
343
|
-
iframe.style.height = `${bounded}px`;
|
|
344
|
-
}
|
|
345
|
-
return {
|
|
346
|
-
mount,
|
|
347
|
-
open,
|
|
348
|
-
close,
|
|
349
|
-
destroy,
|
|
350
|
-
send,
|
|
351
|
-
call,
|
|
352
|
-
on,
|
|
353
|
-
off,
|
|
354
|
-
update,
|
|
355
|
-
getState
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
function validateConfig(config) {
|
|
359
|
-
if (!config.url || !config.allowedOrigin || !config.target) {
|
|
360
|
-
throw new PortError("INVALID_CONFIG", "url, target, and allowedOrigin are required");
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
return __toCommonJS(src_exports);
|
|
364
|
-
})();
|
|
365
|
-
//# sourceMappingURL=index.global.js.map
|
package/dist/index.global.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["export { createPort } from './host';\nexport { PortError } from './errors';\nexport type { Port } from './host';\nexport type { PortConfig, PortErrorCode, PortMessage, PortState } from './types';\n","import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEO,MAAM,UAAN,MAAc;AAAA,IAAd;AACL,WAAQ,YAAY,oBAAI,IAA+B;AAAA;AAAA,IAEvD,GAAG,MAAc,SAA6B;AAC5C,YAAM,MAAM,KAAK,UAAU,IAAI,IAAI,KAAK,oBAAI,IAAkB;AAC9D,UAAI,IAAI,OAAO;AACf,WAAK,UAAU,IAAI,MAAM,GAAG;AAAA,IAC9B;AAAA,IAEA,IAAI,MAAc,SAA6B;AAC7C,WAAK,UAAU,IAAI,IAAI,GAAG,OAAO,OAAO;AAAA,IAC1C;AAAA,IAEA,MAAM,KAAK,MAAc,SAAiC;AACxD,YAAM,OAAO,KAAK,UAAU,IAAI,IAAI;AACpC,UAAI,CAAC,MAAM;AACT;AAAA,MACF;AACA,YAAM,QAAQ,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,OAAO,YAAY,QAAQ,OAAO,CAAC,CAAC;AAAA,IACtE;AAAA,EACF;;;ACpBO,MAAM,YAAN,cAAwB,MAAM;AAAA,IAGnC,YAAY,MAAqB,SAAiB;AAChD,YAAM,OAAO;AACb,WAAK,OAAO;AACZ,WAAK,OAAO;AAAA,IACd;AAAA,EACF;;;ACRO,MAAM,WAAW;AACjB,MAAM,UAAU;AAEhB,WAAS,SAAS,SAAS,OAAe;AAC/C,WAAO,GAAG,MAAM,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,EAC7D;AAEO,WAAS,cAAc,OAAsC;AAClE,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,aAAO;AAAA,IACT;AAEA,UAAM,OAAO;AACb,WACE,KAAK,aAAa,YAClB,KAAK,YAAY,WACjB,OAAO,KAAK,eAAe,YAC3B,OAAO,KAAK,cAAc,YAC1B,OAAO,KAAK,SAAS,YACrB,OAAO,KAAK,SAAS;AAAA,EAEzB;;;ACZA,MAAM,4BAA4B;AAClC,MAAM,uBAAuB;AAC7B,MAAM,8BAA8B;AAe7B,WAAS,WAAW,OAAyB;AAClD,mBAAe,KAAK;AAEpB,UAAM,SAGS;AAAA,MACb,GAAG;AAAA,MACH,MAAM,MAAM,QAAQ;AAAA,MACpB,oBAAoB,MAAM,sBAAsB;AAAA,MAChD,eAAe,MAAM,iBAAiB;AAAA,MACtC,qBAAqB,MAAM,uBAAuB;AAAA,MAClD,WAAW,MAAM,aAAa;AAAA,MAC9B,WAAW,MAAM,aAAa,OAAO;AAAA,IACvC;AAEA,UAAM,aAAa,SAAS,MAAM;AAClC,UAAM,UAAU,IAAI,QAAQ;AAC5B,UAAM,UAAU,oBAAI,IAAyB;AAC7C,QAAI,QAAmB;AACvB,QAAI,SAAmC;AACvC,QAAI,aAAiC;AACrC,QAAI,YAAmC;AAEvC,UAAM,WAAW,CAAC,UAA8B;AAC9C,UAAI,CAAC,QAAQ,iBAAiB,MAAM,WAAW,OAAO,eAAe;AACnE;AAAA,MACF;AAEA,UAAI,MAAM,WAAW,OAAO,eAAe;AACzC;AAAA,MACF;AAEA,UAAI,CAAC,cAAc,MAAM,IAAI,GAAG;AAC9B;AAAA,MACF;AAEA,YAAM,MAAM,MAAM;AAClB,UAAI,IAAI,eAAe,YAAY;AACjC;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,YAAY,IAAI,SAAS,cAAc;AACtD,YAAI,UAAU,eAAe;AAC3B,kBAAQ;AAAA,QACV;AACA;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,cAAc,IAAI,SAAS;AAC1C,cAAMA,QAAO,QAAQ,IAAI,IAAI,OAAO;AACpC,YAAI,CAACA,OAAM;AACT;AAAA,QACF;AACA,qBAAaA,MAAK,OAAO;AACzB,gBAAQ,OAAO,IAAI,OAAO;AAC1B,QAAAA,MAAK,QAAQ,IAAI,OAAO;AACxB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW,IAAI,SAAS;AACvC,cAAMA,QAAO,QAAQ,IAAI,IAAI,OAAO;AACpC,YAAI,CAACA,OAAM;AACT;AAAA,QACF;AACA,qBAAaA,MAAK,OAAO;AACzB,gBAAQ,OAAO,IAAI,OAAO;AAC1B,cAAM,SAAS,OAAO,IAAI,YAAY,WAAW,IAAI,UAAU;AAC/D,QAAAA,MAAK,OAAO,IAAI,UAAU,oBAAoB,MAAM,CAAC;AACrD;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,WAAW,IAAI,SAAS,eAAe;AACtD,oBAAY,IAAI,OAAO;AACvB;AAAA,MACF;AAEA,UAAI,IAAI,SAAS,SAAS;AACxB,aAAK,QAAQ,KAAK,IAAI,MAAM,IAAI,OAAO;AAAA,MACzC;AAAA,IACF;AAEA,WAAO,iBAAiB,WAAW,QAAQ;AAE3C,aAAS,YAAY,OAAoB,YAA0B;AACjE,UAAI,CAAC,MAAM,SAAS,KAAK,GAAG;AAC1B,cAAM,IAAI,UAAU,iBAAiB,UAAU,UAAU,eAAe,KAAK,EAAE;AAAA,MACjF;AAAA,IACF;AAEA,aAAS,cAAc,QAA2C;AAChE,UAAI,OAAO,WAAW,UAAU;AAC9B,cAAM,OAAO,SAAS,cAA2B,MAAM;AACvD,YAAI,CAAC,MAAM;AACT,gBAAM,IAAI,UAAU,kBAAkB,UAAU,MAAM,gBAAgB;AAAA,QACxE;AACA,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAEA,mBAAe,QAAuB;AACpC,kBAAY,CAAC,MAAM,GAAG,OAAO;AAC7B,cAAQ;AAER,mBAAa,cAAc,OAAO,MAAM;AACxC,eAAS,SAAS,cAAc,QAAQ;AACxC,aAAO,MAAM,OAAO;AACpB,aAAO,MAAM,QAAQ;AACrB,aAAO,MAAM,SAAS;AACtB,aAAO,MAAM,UAAU,OAAO,SAAS,UAAU,SAAS;AAE1D,UAAI,OAAO,SAAS,SAAS;AAC3B,oBAAY,SAAS,cAAc,KAAK;AACxC,kBAAU,MAAM,WAAW;AAC3B,kBAAU,MAAM,QAAQ;AACxB,kBAAU,MAAM,aAAa;AAC7B,kBAAU,MAAM,UAAU;AAC1B,kBAAU,MAAM,aAAa;AAC7B,kBAAU,MAAM,iBAAiB;AAEjC,cAAM,YAAY,SAAS,cAAc,KAAK;AAC9C,kBAAU,MAAM,QAAQ;AACxB,kBAAU,MAAM,SAAS;AACzB,kBAAU,MAAM,aAAa;AAC7B,kBAAU,MAAM,eAAe;AAC/B,kBAAU,MAAM,WAAW;AAC3B,eAAO,MAAM,UAAU;AACvB,eAAO,MAAM,SAAS;AAEtB,kBAAU,OAAO,MAAM;AACvB,kBAAU,OAAO,SAAS;AAC1B,mBAAW,OAAO,SAAS;AAE3B,kBAAU,iBAAiB,SAAS,CAAC,UAAU;AAC7C,cAAI,MAAM,WAAW,WAAW;AAC9B,iBAAK,MAAM;AAAA,UACb;AAAA,QACF,CAAC;AAED,eAAO,iBAAiB,WAAW,CAAC,UAAU;AAC5C,cAAI,MAAM,QAAQ,YAAY,UAAU,QAAQ;AAC9C,iBAAK,MAAM;AAAA,UACb;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,mBAAW,OAAO,MAAM;AAAA,MAC1B;AAEA,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,cAAM,QAAQ,WAAW,MAAM;AAC7B,iBAAO,IAAI,UAAU,uBAAuB,6BAA6B,CAAC;AAAA,QAC5E,GAAG,OAAO,mBAAmB;AAE7B,gBAAQ;AAAA,UACN;AAAA,UACA,MAAM;AACJ,yBAAa,KAAK;AAClB,oBAAQ;AAAA,UACV;AAAA,UACA,EAAE,MAAM,KAAK;AAAA,QACf;AAAA,MACF,CAAC;AAED,cAAQ;AACR,YAAM,UAAU;AAAA,IAClB;AAEA,mBAAe,YAA2B;AACxC,kBAAY,CAAC,SAAS,GAAG,WAAW;AACpC,UAAI,CAAC,QAAQ,eAAe;AAC1B,cAAM,IAAI,UAAU,iBAAiB,qCAAqC;AAAA,MAC5E;AAEA,cAAQ;AACR,WAAK,EAAE,MAAM,UAAU,MAAM,aAAa,CAAC;AAE3C,YAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,cAAM,QAAQ,WAAW,MAAM;AAC7B,wBAAc,IAAI;AAClB,iBAAO,IAAI,UAAU,qBAAqB,qBAAqB,CAAC;AAAA,QAClE,GAAG,OAAO,kBAAkB;AAE5B,cAAM,OAAO,YAAY,MAAM;AAC7B,cAAI,UAAU,SAAS;AACrB,yBAAa,KAAK;AAClB,0BAAc,IAAI;AAClB,oBAAQ;AAAA,UACV;AAAA,QACF,GAAG,EAAE;AAAA,MACP,CAAC;AAED,UAAI,OAAO,SAAS,UAAU;AAC5B,gBAAQ;AAAA,MACV;AAAA,IACF;AAEA,aAAS,OAAsB;AAC7B,kBAAY,CAAC,SAAS,QAAQ,GAAG,MAAM;AACvC,UAAI,OAAO,SAAS,SAAS;AAC3B,gBAAQ;AACR,eAAO,QAAQ,QAAQ;AAAA,MACzB;AACA,UAAI,CAAC,WAAW;AACd,cAAM,IAAI,UAAU,iBAAiB,oBAAoB;AAAA,MAC3D;AACA,gBAAU,MAAM,UAAU;AAC1B,cAAQ;AACR,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,aAAS,QAAuB;AAC9B,kBAAY,CAAC,MAAM,GAAG,OAAO;AAC7B,UAAI,OAAO,SAAS,WAAW,WAAW;AACxC,kBAAU,MAAM,UAAU;AAAA,MAC5B;AACA,cAAQ;AACR,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAEA,aAAS,UAAgB;AACvB,UAAI,UAAU,aAAa;AACzB;AAAA,MACF;AAEA,cAAQ,QAAQ,CAAC,UAAU;AACzB,qBAAa,MAAM,OAAO;AAC1B,cAAM,OAAO,IAAI,UAAU,kBAAkB,yBAAyB,CAAC;AAAA,MACzE,CAAC;AACD,cAAQ,MAAM;AAEd,aAAO,oBAAoB,WAAW,QAAQ;AAC9C,cAAQ,OAAO;AACf,iBAAW,OAAO;AAClB,eAAS;AACT,kBAAY;AACZ,mBAAa;AACb,cAAQ;AAAA,IACV;AAEA,aAAS,KAAK,SAA2E;AACvF,UAAI,CAAC,QAAQ,eAAe;AAC1B,cAAM,IAAI,UAAU,iBAAiB,yBAAyB;AAAA,MAChE;AAEA,YAAM,eAA4B;AAAA,QAChC,UAAU;AAAA,QACV,SAAS;AAAA,QACT;AAAA,QACA,WAAW,SAAS;AAAA,QACpB,GAAG;AAAA,MACL;AAEA,aAAO,cAAc,YAAY,cAAc,OAAO,aAAa;AAAA,IACrE;AAEA,aAAS,KAAK,MAAc,SAAyB;AACnD,UAAI,UAAU,aAAa;AACzB,cAAM,IAAI,UAAU,kBAAkB,mBAAmB;AAAA,MAC3D;AAEA,kBAAY,CAAC,SAAS,QAAQ,QAAQ,GAAG,MAAM;AAC/C,WAAK,EAAE,MAAM,SAAS,MAAM,QAAQ,CAAC;AAAA,IACvC;AAEA,aAAS,KAAkB,MAAc,SAA+B;AACtE,UAAI,UAAU,aAAa;AACzB,eAAO,QAAQ,OAAO,IAAI,UAAU,kBAAkB,mBAAmB,CAAC;AAAA,MAC5E;AAEA,kBAAY,CAAC,SAAS,QAAQ,QAAQ,GAAG,MAAM;AAE/C,YAAM,YAAY,SAAS;AAC3B,YAAM,UAAuB;AAAA,QAC3B,UAAU;AAAA,QACV,SAAS;AAAA,QACT;AAAA,QACA;AAAA,QACA,MAAM;AAAA,QACN;AAAA,QACA;AAAA,MACF;AAEA,cAAQ,eAAe,YAAY,SAAS,OAAO,aAAa;AAEhE,aAAO,IAAI,QAAW,CAAC,SAAS,WAAW;AACzC,cAAM,UAAU,WAAW,MAAM;AAC/B,kBAAQ,OAAO,SAAS;AACxB,iBAAO,IAAI,UAAU,gBAAgB,GAAG,IAAI,YAAY,CAAC;AAAA,QAC3D,GAAG,OAAO,aAAa;AAEvB,gBAAQ,IAAI,WAAW,EAAE,SAA8C,QAAQ,QAAQ,CAAC;AAAA,MAC1F,CAAC;AAAA,IACH;AAEA,aAAS,GAAG,MAAc,SAA6B;AACrD,cAAQ,GAAG,MAAM,OAAO;AAAA,IAC1B;AAEA,aAAS,IAAI,MAAc,SAA6B;AACtD,cAAQ,IAAI,MAAM,OAAO;AAAA,IAC3B;AAEA,aAAS,OAAO,MAAiC;AAC/C,aAAO,OAAO,QAAQ,IAAI;AAAA,IAC5B;AAEA,aAAS,WAAsB;AAC7B,aAAO;AAAA,IACT;AAEA,aAAS,YAAY,SAAwB;AAC3C,UAAI,CAAC,QAAQ;AACX;AAAA,MACF;AACA,UAAI,OAAO,YAAY,YAAY,OAAO,MAAM,OAAO,GAAG;AACxD;AAAA,MACF;AAEA,YAAM,UAAU,KAAK,IAAI,OAAO,WAAW,KAAK,IAAI,OAAO,WAAW,OAAO,CAAC;AAC9E,aAAO,MAAM,SAAS,GAAG,OAAO;AAAA,IAClC;AAEA,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,WAAS,eAAe,QAA0B;AAChD,QAAI,CAAC,OAAO,OAAO,CAAC,OAAO,iBAAiB,CAAC,OAAO,QAAQ;AAC1D,YAAM,IAAI,UAAU,kBAAkB,6CAA6C;AAAA,IACrF;AAAA,EACF;","names":["call"]}
|
package/dist/index.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/emitter.ts","../src/errors.ts","../src/utils.ts","../src/host.ts"],"sourcesContent":["import type { EventHandler } from './types';\n\nexport class Emitter {\n private listeners = new Map<string, Set<EventHandler>>();\n\n on(type: string, handler: EventHandler): void {\n const set = this.listeners.get(type) ?? new Set<EventHandler>();\n set.add(handler);\n this.listeners.set(type, set);\n }\n\n off(type: string, handler: EventHandler): void {\n this.listeners.get(type)?.delete(handler);\n }\n\n async emit(type: string, payload: unknown): Promise<void> {\n const list = this.listeners.get(type);\n if (!list) {\n return;\n }\n await Promise.all([...list].map(async (handler) => handler(payload)));\n }\n}\n","import type { PortErrorCode } from './types';\n\nexport class PortError extends Error {\n readonly code: PortErrorCode;\n\n constructor(code: PortErrorCode, message: string) {\n super(message);\n this.code = code;\n this.name = 'PortError';\n }\n}\n","import type { PortMessage } from './types';\n\nexport const PROTOCOL = 'crup.port';\nexport const VERSION = '1';\n\nexport function randomId(prefix = 'msg'): string {\n return `${prefix}_${Math.random().toString(36).slice(2, 10)}`;\n}\n\nexport function isPortMessage(value: unknown): value is PortMessage {\n if (typeof value !== 'object' || value === null) {\n return false;\n }\n\n const data = value as Partial<PortMessage>;\n return (\n data.protocol === PROTOCOL &&\n data.version === VERSION &&\n typeof data.instanceId === 'string' &&\n typeof data.messageId === 'string' &&\n typeof data.kind === 'string' &&\n typeof data.type === 'string'\n );\n}\n","import { Emitter } from './emitter';\nimport { PortError } from './errors';\nimport type { EventHandler, PortConfig, PortMessage, PortState } from './types';\nimport { isPortMessage, PROTOCOL, randomId, VERSION } from './utils';\n\ninterface PendingCall {\n resolve: (value: unknown) => void;\n reject: (reason?: unknown) => void;\n timeout: ReturnType<typeof setTimeout>;\n}\n\nconst DEFAULT_HANDSHAKE_TIMEOUT = 8_000;\nconst DEFAULT_CALL_TIMEOUT = 8_000;\nconst DEFAULT_IFRAME_LOAD_TIMEOUT = 8_000;\n\nexport interface Port {\n mount(): Promise<void>;\n open(): Promise<void>;\n close(): Promise<void>;\n destroy(): void;\n send(type: string, payload?: unknown): void;\n call<T = unknown>(type: string, payload?: unknown): Promise<T>;\n on(type: string, handler: EventHandler): void;\n off(type: string, handler: EventHandler): void;\n update(config: Partial<PortConfig>): void;\n getState(): PortState;\n}\n\nexport function createPort(input: PortConfig): Port {\n validateConfig(input);\n\n const config: Required<\n Pick<PortConfig, 'mode' | 'handshakeTimeoutMs' | 'callTimeoutMs' | 'iframeLoadTimeoutMs' | 'minHeight' | 'maxHeight'>\n > &\n PortConfig = {\n ...input,\n mode: input.mode ?? 'inline',\n handshakeTimeoutMs: input.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT,\n callTimeoutMs: input.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT,\n iframeLoadTimeoutMs: input.iframeLoadTimeoutMs ?? DEFAULT_IFRAME_LOAD_TIMEOUT,\n minHeight: input.minHeight ?? 0,\n maxHeight: input.maxHeight ?? Number.MAX_SAFE_INTEGER\n };\n\n const instanceId = randomId('port');\n const emitter = new Emitter();\n const pending = new Map<string, PendingCall>();\n let state: PortState = 'idle';\n let iframe: HTMLIFrameElement | null = null;\n let targetNode: HTMLElement | null = null;\n let modalRoot: HTMLDivElement | null = null;\n\n const listener = (event: MessageEvent): void => {\n if (!iframe?.contentWindow || event.source !== iframe.contentWindow) {\n return;\n }\n\n if (event.origin !== config.allowedOrigin) {\n return;\n }\n\n if (!isPortMessage(event.data)) {\n return;\n }\n\n const msg = event.data;\n if (msg.instanceId !== instanceId) {\n return;\n }\n\n if (msg.kind === 'system' && msg.type === 'port:ready') {\n if (state === 'handshaking') {\n state = 'ready';\n }\n return;\n }\n\n if (msg.kind === 'response' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n call.resolve(msg.payload);\n return;\n }\n\n if (msg.kind === 'error' && msg.replyTo) {\n const call = pending.get(msg.replyTo);\n if (!call) {\n return;\n }\n clearTimeout(call.timeout);\n pending.delete(msg.replyTo);\n const reason = typeof msg.payload === 'string' ? msg.payload : 'Rejected';\n call.reject(new PortError('MESSAGE_REJECTED', reason));\n return;\n }\n\n if (msg.kind === 'event' && msg.type === 'port:resize') {\n applyResize(msg.payload);\n return;\n }\n\n if (msg.kind === 'event') {\n void emitter.emit(msg.type, msg.payload);\n }\n };\n\n window.addEventListener('message', listener);\n\n function ensureState(valid: PortState[], nextAction: string): void {\n if (!valid.includes(state)) {\n throw new PortError('INVALID_STATE', `Cannot ${nextAction} from state ${state}`);\n }\n }\n\n function resolveTarget(target: PortConfig['target']): HTMLElement {\n if (typeof target === 'string') {\n const node = document.querySelector<HTMLElement>(target);\n if (!node) {\n throw new PortError('INVALID_CONFIG', `Target ${target} was not found`);\n }\n return node;\n }\n return target;\n }\n\n async function mount(): Promise<void> {\n ensureState(['idle'], 'mount');\n state = 'mounting';\n\n targetNode = resolveTarget(config.target);\n iframe = document.createElement('iframe');\n iframe.src = config.url;\n iframe.style.width = '100%';\n iframe.style.border = '0';\n iframe.style.display = config.mode === 'modal' ? 'none' : 'block';\n\n if (config.mode === 'modal') {\n modalRoot = document.createElement('div');\n modalRoot.style.position = 'fixed';\n modalRoot.style.inset = '0';\n modalRoot.style.background = 'rgba(0,0,0,0.5)';\n modalRoot.style.display = 'none';\n modalRoot.style.alignItems = 'center';\n modalRoot.style.justifyContent = 'center';\n\n const container = document.createElement('div');\n container.style.width = 'min(900px, 95vw)';\n container.style.height = 'min(85vh, 900px)';\n container.style.background = '#fff';\n container.style.borderRadius = '8px';\n container.style.overflow = 'hidden';\n iframe.style.display = 'block';\n iframe.style.height = '100%';\n\n container.append(iframe);\n modalRoot.append(container);\n targetNode.append(modalRoot);\n\n modalRoot.addEventListener('click', (event) => {\n if (event.target === modalRoot) {\n void close();\n }\n });\n\n window.addEventListener('keydown', (event) => {\n if (event.key === 'Escape' && state === 'open') {\n void close();\n }\n });\n } else {\n targetNode.append(iframe);\n }\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n reject(new PortError('IFRAME_LOAD_TIMEOUT', 'iframe did not load in time'));\n }, config.iframeLoadTimeoutMs);\n\n iframe?.addEventListener(\n 'load',\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true }\n );\n });\n\n state = 'mounted';\n await handshake();\n }\n\n async function handshake(): Promise<void> {\n ensureState(['mounted'], 'handshake');\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is unavailable for handshake');\n }\n\n state = 'handshaking';\n post({ kind: 'system', type: 'port:hello' });\n\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n clearInterval(poll);\n reject(new PortError('HANDSHAKE_TIMEOUT', 'handshake timed out'));\n }, config.handshakeTimeoutMs);\n\n const poll = setInterval(() => {\n if (state === 'ready') {\n clearTimeout(timer);\n clearInterval(poll);\n resolve();\n }\n }, 10);\n });\n\n if (config.mode === 'inline') {\n state = 'open';\n }\n }\n\n function open(): Promise<void> {\n ensureState(['ready', 'closed'], 'open');\n if (config.mode !== 'modal') {\n state = 'open';\n return Promise.resolve();\n }\n if (!modalRoot) {\n throw new PortError('INVALID_STATE', 'modal root missing');\n }\n modalRoot.style.display = 'flex';\n state = 'open';\n return Promise.resolve();\n }\n\n function close(): Promise<void> {\n ensureState(['open'], 'close');\n if (config.mode === 'modal' && modalRoot) {\n modalRoot.style.display = 'none';\n }\n state = 'closed';\n return Promise.resolve();\n }\n\n function destroy(): void {\n if (state === 'destroyed') {\n return;\n }\n\n pending.forEach((entry) => {\n clearTimeout(entry.timeout);\n entry.reject(new PortError('PORT_DESTROYED', 'Port has been destroyed'));\n });\n pending.clear();\n\n window.removeEventListener('message', listener);\n iframe?.remove();\n modalRoot?.remove();\n iframe = null;\n modalRoot = null;\n targetNode = null;\n state = 'destroyed';\n }\n\n function post(message: Pick<PortMessage, 'kind' | 'type' | 'payload' | 'replyTo'>): void {\n if (!iframe?.contentWindow) {\n throw new PortError('INVALID_STATE', 'iframe is not available');\n }\n\n const finalMessage: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId: randomId(),\n ...message\n };\n\n iframe.contentWindow.postMessage(finalMessage, config.allowedOrigin);\n }\n\n function send(type: string, payload?: unknown): void {\n if (state === 'destroyed') {\n throw new PortError('PORT_DESTROYED', 'Port is destroyed');\n }\n\n ensureState(['ready', 'open', 'closed'], 'send');\n post({ kind: 'event', type, payload });\n }\n\n function call<T = unknown>(type: string, payload?: unknown): Promise<T> {\n if (state === 'destroyed') {\n return Promise.reject(new PortError('PORT_DESTROYED', 'Port is destroyed'));\n }\n\n ensureState(['ready', 'open', 'closed'], 'call');\n\n const messageId = randomId();\n const message: PortMessage = {\n protocol: PROTOCOL,\n version: VERSION,\n instanceId,\n messageId,\n kind: 'request',\n type,\n payload\n };\n\n iframe?.contentWindow?.postMessage(message, config.allowedOrigin);\n\n return new Promise<T>((resolve, reject) => {\n const timeout = setTimeout(() => {\n pending.delete(messageId);\n reject(new PortError('CALL_TIMEOUT', `${type} timed out`));\n }, config.callTimeoutMs);\n\n pending.set(messageId, { resolve: resolve as (value: unknown) => void, reject, timeout });\n });\n }\n\n function on(type: string, handler: EventHandler): void {\n emitter.on(type, handler);\n }\n\n function off(type: string, handler: EventHandler): void {\n emitter.off(type, handler);\n }\n\n function update(next: Partial<PortConfig>): void {\n Object.assign(config, next);\n }\n\n function getState(): PortState {\n return state;\n }\n\n function applyResize(payload: unknown): void {\n if (!iframe) {\n return;\n }\n if (typeof payload !== 'number' || Number.isNaN(payload)) {\n return;\n }\n\n const bounded = Math.max(config.minHeight, Math.min(config.maxHeight, payload));\n iframe.style.height = `${bounded}px`;\n }\n\n return {\n mount,\n open,\n close,\n destroy,\n send,\n call,\n on,\n off,\n update,\n getState\n };\n}\n\nfunction validateConfig(config: PortConfig): void {\n if (!config.url || !config.allowedOrigin || !config.target) {\n throw new PortError('INVALID_CONFIG', 'url, target, and allowedOrigin are required');\n }\n}\n"],"mappings":"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","names":["call"]}
|