@cp949/iframecall 0.1.0 → 0.2.1
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/CHANGELOG.md +36 -0
- package/README.md +120 -58
- package/dist/{chunk-UYZXYOI6.js → chunk-RSUK22E2.js} +4 -2
- package/dist/host.d.ts +2 -2
- package/dist/host.js +85 -72
- package/dist/iframe.d.ts +9 -9
- package/dist/iframe.js +40 -19
- package/dist/{messages-Cg4n93bf.d.ts → messages-uOmcU6eT.d.ts} +8 -6
- package/package.json +27 -18
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
이 프로젝트의 모든 주요 변경 사항은 이 파일에 기록됩니다.
|
|
4
|
+
|
|
5
|
+
형식은 [Keep a Changelog](https://keepachangelog.com/ko/1.1.0/)을 따르며, 버전 관리는 [Semantic Versioning](https://semver.org/spec/v2.0.0.html)을 준수합니다. 0.x 버전대에서는 호환성이 깨지는 변경도 minor 버전 증가로 처리합니다.
|
|
6
|
+
|
|
7
|
+
## [0.2.1] - 2026-08-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- host controller의 정식 command 실행 API로 `invoke(command, args, options?)`를 추가했습니다.
|
|
12
|
+
|
|
13
|
+
### Deprecated
|
|
14
|
+
|
|
15
|
+
- `controller.call()`은 호환성을 위해 유지하지만 다음 major release에서 제거할 예정입니다. `controller.invoke()`를 사용하세요.
|
|
16
|
+
|
|
17
|
+
## [0.2.0] - 2026-04-28
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `$` 네임스페이스와 `$onCommandRun` wrap hook 도입 — 명령 실행 전후를 감싸는 미들웨어 형태의 훅을 등록할 수 있습니다.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- 라이프사이클/도메인 채널의 책임을 분리하고, 라이프사이클 ready 알림 API를 `sendLifecycleReady`로 정리했습니다. **(Breaking)**
|
|
26
|
+
|
|
27
|
+
### BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
- 라이프사이클 ready 전송 API의 식별자가 `sendLifecycleReady`로 리네임되었습니다. 이전 명칭을 직접 호출하던 코드는 새 이름으로 갱신해야 합니다.
|
|
30
|
+
|
|
31
|
+
## [0.1.0] - 2026-04-27
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `@cp949/iframecall` 최초 공개 — 타입 안전한 host ↔ iframe postMessage 호출 라이브러리.
|
|
36
|
+
- React 18/19 호환 진입점(`./host`, `./iframe`) 제공.
|
package/README.md
CHANGED
|
@@ -20,16 +20,16 @@ yarn add @cp949/iframecall
|
|
|
20
20
|
|
|
21
21
|
`react`, `@types/react`는 peer dependency이다. 프로젝트에 이미 설치되어 있어야 한다.
|
|
22
22
|
|
|
23
|
-
| peer
|
|
24
|
-
|
|
25
|
-
| `react`
|
|
23
|
+
| peer | 지원 버전 |
|
|
24
|
+
| -------------- | ---------------------- |
|
|
25
|
+
| `react` | `^18.0.0 \|\| ^19.0.0` |
|
|
26
26
|
| `@types/react` | `^18.0.0 \|\| ^19.0.0` |
|
|
27
27
|
|
|
28
28
|
## 진입점
|
|
29
29
|
|
|
30
|
-
| import
|
|
31
|
-
|
|
32
|
-
| `@cp949/iframecall/host`
|
|
30
|
+
| import | 용도 |
|
|
31
|
+
| -------------------------- | ------------------------------------------------- |
|
|
32
|
+
| `@cp949/iframecall/host` | 부모 페이지(host)에서 iframe을 제어할 때 |
|
|
33
33
|
| `@cp949/iframecall/iframe` | 임베드된 페이지(iframe)에서 host의 호출을 받을 때 |
|
|
34
34
|
|
|
35
35
|
호스트와 iframe은 서로 다른 origin에서 실행되며, 각 진입점은 그쪽에서만 필요한 API와 타입만 노출한다.
|
|
@@ -41,17 +41,19 @@ yarn add @cp949/iframecall
|
|
|
41
41
|
host와 iframe 양쪽에서 동일하게 사용할 커맨드와 알림 타입을 정의한다.
|
|
42
42
|
|
|
43
43
|
```ts
|
|
44
|
-
type
|
|
44
|
+
type AppCommands = {
|
|
45
45
|
greet(name: string): Promise<string>;
|
|
46
46
|
add(a: number, b: number): Promise<number>;
|
|
47
47
|
delay(ms: number): Promise<void>;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
-
type
|
|
51
|
-
"status-changed":
|
|
50
|
+
type AppNotifications = {
|
|
51
|
+
"status-changed": "idle" | "processing";
|
|
52
52
|
};
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
> 라이프사이클(`ready` / `terminated`)과 도메인 알림은 채널을 분리한다. 도메인 페이로드에 `"ready"`를 넣지 않는다 — `ready`는 transport가 살아 있다는 라이프사이클 신호 전용이다.
|
|
56
|
+
|
|
55
57
|
> 별도 공유 패키지 없이 양쪽에 같은 타입을 두는 패턴을 권장한다. 모노레포라면 공통 패키지로 빼도 된다.
|
|
56
58
|
|
|
57
59
|
### 2. host 측 (부모 페이지)
|
|
@@ -66,8 +68,8 @@ const IFRAME_ORIGIN = "https://iframe.example.com";
|
|
|
66
68
|
|
|
67
69
|
export function HostPage() {
|
|
68
70
|
const { iframeRef, controller, status } = useIframeCallController<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
AppCommands,
|
|
72
|
+
AppNotifications
|
|
71
73
|
>({
|
|
72
74
|
targetOrigin: IFRAME_ORIGIN,
|
|
73
75
|
allowedOrigins: [IFRAME_ORIGIN],
|
|
@@ -82,7 +84,7 @@ export function HostPage() {
|
|
|
82
84
|
|
|
83
85
|
const handleGreet = async () => {
|
|
84
86
|
if (!controller) return;
|
|
85
|
-
const result = await controller.
|
|
87
|
+
const result = await controller.invoke("greet", ["World"]);
|
|
86
88
|
console.log(result); // "Hello, World!"
|
|
87
89
|
};
|
|
88
90
|
|
|
@@ -92,11 +94,7 @@ export function HostPage() {
|
|
|
92
94
|
<button type="button" onClick={handleGreet} disabled={status !== "ready"}>
|
|
93
95
|
Greet
|
|
94
96
|
</button>
|
|
95
|
-
<iframe
|
|
96
|
-
ref={iframeRef}
|
|
97
|
-
src={IFRAME_ORIGIN}
|
|
98
|
-
title="iframe demo"
|
|
99
|
-
/>
|
|
97
|
+
<iframe ref={iframeRef} src={IFRAME_ORIGIN} title="iframe demo" />
|
|
100
98
|
</div>
|
|
101
99
|
);
|
|
102
100
|
}
|
|
@@ -104,7 +102,19 @@ export function HostPage() {
|
|
|
104
102
|
|
|
105
103
|
### 3. iframe 측 (임베드된 페이지)
|
|
106
104
|
|
|
107
|
-
커맨드 구현은 클래스로 정의한다. 생성자는 `iframeHelper`를 인자로 받으며, 메서드 이름이 곧 커맨드 이름이 된다.
|
|
105
|
+
커맨드 구현은 클래스로 정의한다. 생성자는 `iframeHelper`를 인자로 받으며, prototype에 둔 메서드 이름이 곧 커맨드 이름이 된다.
|
|
106
|
+
|
|
107
|
+
prefix 컨벤션:
|
|
108
|
+
|
|
109
|
+
| prefix | 의미 |
|
|
110
|
+
| ------ | ---------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| (없음) | host로 dispatch되는 remote command |
|
|
112
|
+
| `_` | 사용자 local-only 메서드. dispatch 대상에서 제외된다. (예: `_sendLifecycleReady`, `_onStatusChange`) |
|
|
113
|
+
| `$` | 라이브러리 점유 namespace. dispatch에서 제외되며, 라이브러리가 정의한 hook 이름만 의미가 있다. |
|
|
114
|
+
|
|
115
|
+
현재 라이브러리가 인식하는 hook은 한 개:
|
|
116
|
+
|
|
117
|
+
- `$onCommandRun(cmd, args, invoke)` — 매 command dispatch를 wrap한다. `await invoke()`를 try/finally로 감싸 상태 토글·로깅·refcount 등을 한 곳에서 처리한다.
|
|
108
118
|
|
|
109
119
|
```tsx
|
|
110
120
|
"use client";
|
|
@@ -117,14 +127,53 @@ import { useEffect } from "react";
|
|
|
117
127
|
|
|
118
128
|
const HOST_ORIGIN = "https://host.example.com";
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
type RunningStatus = "idle" | "processing";
|
|
131
|
+
|
|
132
|
+
// 클래스 자체가 곧 command 타입이다. 별도 interface를 두지 않는다.
|
|
133
|
+
// `_` prefix는 사용자 local-only(dispatch 제외), `$` prefix는 라이브러리 namespace.
|
|
134
|
+
class AppCommands {
|
|
135
|
+
private status: RunningStatus = "idle";
|
|
136
|
+
private inflight = 0;
|
|
137
|
+
private listeners = new Set<(s: RunningStatus) => void>();
|
|
138
|
+
|
|
139
|
+
constructor(private iframeHelper: IframeHelper<AppNotifications>) {}
|
|
140
|
+
|
|
141
|
+
_sendLifecycleReady(): void {
|
|
142
|
+
this.iframeHelper.sendLifecycleReady();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
_onStatusChange(fn: (s: RunningStatus) => void): () => void {
|
|
146
|
+
this.listeners.add(fn);
|
|
147
|
+
return () => {
|
|
148
|
+
this.listeners.delete(fn);
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 모든 command를 wrap. 동시 dispatch에서 안쪽 호출이 끝나기 전에 idle로 떨어지지 않도록 refcount.
|
|
153
|
+
async $onCommandRun(
|
|
154
|
+
_cmd: string,
|
|
155
|
+
_args: readonly unknown[],
|
|
156
|
+
invoke: () => Promise<unknown>,
|
|
157
|
+
): Promise<unknown> {
|
|
158
|
+
this.inflight += 1;
|
|
159
|
+
if (this.inflight === 1) this._setStatus("processing");
|
|
160
|
+
try {
|
|
161
|
+
return await invoke();
|
|
162
|
+
} finally {
|
|
163
|
+
this.inflight -= 1;
|
|
164
|
+
if (this.inflight === 0) this._setStatus("idle");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private _setStatus(next: RunningStatus): void {
|
|
169
|
+
if (this.status === next) return;
|
|
170
|
+
this.status = next;
|
|
171
|
+
this.iframeHelper.sendNotificationToHost("status-changed", next);
|
|
172
|
+
for (const fn of this.listeners) fn(next);
|
|
173
|
+
}
|
|
122
174
|
|
|
123
175
|
async greet(name: string): Promise<string> {
|
|
124
|
-
|
|
125
|
-
const result = `Hello, ${name}!`;
|
|
126
|
-
this.iframeHelper.sendNotificationToHost("status-changed", "idle");
|
|
127
|
-
return result;
|
|
176
|
+
return `Hello, ${name}!`;
|
|
128
177
|
}
|
|
129
178
|
|
|
130
179
|
async add(a: number, b: number): Promise<number> {
|
|
@@ -132,25 +181,27 @@ class DemoCommandsImpl {
|
|
|
132
181
|
}
|
|
133
182
|
|
|
134
183
|
async delay(ms: number): Promise<void> {
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
184
|
+
await new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
136
185
|
}
|
|
137
186
|
}
|
|
138
187
|
|
|
139
188
|
export function IframePage() {
|
|
140
|
-
const { iframeHelper, isActive } = useIframeCallRunner<
|
|
141
|
-
|
|
142
|
-
|
|
189
|
+
const { iframeHelper, commands, isActive } = useIframeCallRunner<
|
|
190
|
+
AppCommands,
|
|
191
|
+
AppNotifications
|
|
143
192
|
>({
|
|
144
193
|
targetOrigin: HOST_ORIGIN,
|
|
145
194
|
allowedOrigins: [HOST_ORIGIN],
|
|
146
|
-
Commands:
|
|
195
|
+
Commands: AppCommands,
|
|
147
196
|
});
|
|
148
197
|
|
|
149
198
|
useEffect(() => {
|
|
150
|
-
if (!iframeHelper) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
199
|
+
if (!iframeHelper || !commands) return;
|
|
200
|
+
commands._sendLifecycleReady();
|
|
201
|
+
return commands._onStatusChange((s) => {
|
|
202
|
+
console.log("local status:", s);
|
|
203
|
+
});
|
|
204
|
+
}, [iframeHelper, commands]);
|
|
154
205
|
|
|
155
206
|
return <p>{isActive ? "active" : "initializing"}</p>;
|
|
156
207
|
}
|
|
@@ -163,36 +214,39 @@ host iframe
|
|
|
163
214
|
│ │
|
|
164
215
|
│ <iframe src="..."> │
|
|
165
216
|
│──────────────────────────────────────▶│ mount
|
|
166
|
-
│ │
|
|
167
|
-
│ ◀── ready ───────────────────────────│
|
|
217
|
+
│ │ commands._sendLifecycleReady() → sendLifecycleReady()
|
|
218
|
+
│ ◀── ready ───────────────────────────│ (lifecycle 채널)
|
|
168
219
|
│ controller.status = "ready" │
|
|
169
220
|
│ │
|
|
170
|
-
│ controller.
|
|
171
|
-
│ ── request ─────────────────────────▶│
|
|
221
|
+
│ controller.invoke("greet", ["World"])│
|
|
222
|
+
│ ── request ─────────────────────────▶│ $onCommandRun → AppCommands.greet("World")
|
|
223
|
+
│ ◀── notify status-changed:processing │ (도메인 채널, refcount 0→1)
|
|
172
224
|
│ ◀── response: "Hello, World!" ───────│
|
|
173
|
-
│
|
|
174
|
-
│ ◀── notify "status-changed" ─────────│ sendNotificationToHost(...)
|
|
225
|
+
│ ◀── notify status-changed:idle ──────│ (도메인 채널, refcount 1→0)
|
|
175
226
|
```
|
|
176
227
|
|
|
177
|
-
- iframe이 마운트되면 `
|
|
178
|
-
- host의 `controller.
|
|
228
|
+
- iframe이 마운트되면 `commands._sendLifecycleReady()`가 `sendLifecycleReady()`를 통해 transport ready 신호를 보낸다.
|
|
229
|
+
- host의 `controller.invoke`는 ready 시점까지 대기한 뒤 전송된다.
|
|
179
230
|
- 응답은 Promise로 돌아오며, iframe 측 메서드가 throw하면 host 쪽 Promise는 reject된다.
|
|
180
231
|
- iframe → host 단방향 알림은 `sendNotificationToHost`로 보내고, host 쪽에서 `controller.onNotificationFromIframe`으로 받는다.
|
|
232
|
+
- **라이프사이클 채널과 도메인 채널은 책임이 다르다.** `ready`/`terminated`는 transport 신호 전용이고, 도메인 알림(`status-changed` 등)에는 `"ready"` 같은 lifecycle 의미를 담지 않는다.
|
|
233
|
+
- prototype에 `$onCommandRun(cmd, args, invoke)`을 두면 모든 command dispatch가 그 함수로 wrap된다. status 토글·로깅·refcount 같은 횡단 관심사를 한 곳에서 처리할 수 있다.
|
|
181
234
|
|
|
182
235
|
## API 개요
|
|
183
236
|
|
|
184
237
|
### host 진입점
|
|
185
238
|
|
|
186
|
-
| export
|
|
187
|
-
|
|
188
|
-
| `useIframeCallController`
|
|
189
|
-
| `createIframeCallController`
|
|
190
|
-
| `createIframeWindowTransport` | factory | 커스텀 트랜스포트 구성용
|
|
191
|
-
| `consoleDebugLogger`
|
|
239
|
+
| export | 종류 | 설명 |
|
|
240
|
+
| ----------------------------- | ------- | ---------------------------------------------------------------- |
|
|
241
|
+
| `useIframeCallController` | hook | host용 React 훅. `iframeRef`, `controller`, `status`를 반환한다. |
|
|
242
|
+
| `createIframeCallController` | factory | 훅 없이 컨트롤러를 직접 만들 때 사용 |
|
|
243
|
+
| `createIframeWindowTransport` | factory | 커스텀 트랜스포트 구성용 |
|
|
244
|
+
| `consoleDebugLogger` | util | 디버그 이벤트를 콘솔에 출력하는 로거 |
|
|
192
245
|
|
|
193
246
|
훅이 반환하는 `controller`의 주요 멤버:
|
|
194
247
|
|
|
195
|
-
- `controller.
|
|
248
|
+
- `controller.invoke(command, args, options?)` — iframe의 커맨드 호출, 결과를 Promise로 반환
|
|
249
|
+
- `controller.call(command, args, options?)` — deprecated 호환 alias. `invoke`를 사용하세요.
|
|
196
250
|
- `controller.onNotificationFromIframe(event, handler)` — iframe이 보내는 알림 구독, unsubscribe 함수 반환
|
|
197
251
|
- `controller.ready` — iframe ready 신호 대기용 Promise
|
|
198
252
|
- `controller.terminated` — 종료 사유를 노출하는 Promise (정상 dispose면 `null`)
|
|
@@ -201,18 +255,26 @@ host iframe
|
|
|
201
255
|
|
|
202
256
|
### iframe 진입점
|
|
203
257
|
|
|
204
|
-
| export
|
|
205
|
-
|
|
206
|
-
| `useIframeCallRunner`
|
|
207
|
-
| `createIframeCallRunner`
|
|
208
|
-
| `createParentWindowTransport` | factory | 커스텀 트랜스포트 구성용
|
|
258
|
+
| export | 종류 | 설명 |
|
|
259
|
+
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
260
|
+
| `useIframeCallRunner` | hook | iframe용 React 훅. `commands`, `iframeHelper`, `isActive`를 반환한다. mount 전 `commands`/`iframeHelper`는 `null`. |
|
|
261
|
+
| `createIframeCallRunner` | factory | 훅 없이 러너를 직접 만들 때 사용 |
|
|
262
|
+
| `createParentWindowTransport` | factory | 커스텀 트랜스포트 구성용 |
|
|
209
263
|
|
|
210
264
|
`iframeHelper`의 주요 멤버:
|
|
211
265
|
|
|
212
|
-
- `iframeHelper.sendNotificationToHost(event, payload)` — host로 알림 전송 (lifecycle 예약 이름 `ready`/`terminated`는 제외)
|
|
213
|
-
- `iframeHelper.
|
|
266
|
+
- `iframeHelper.sendNotificationToHost(event, payload)` — host로 도메인 알림 전송 (lifecycle 예약 이름 `ready`/`terminated`는 타입에서 제외)
|
|
267
|
+
- `iframeHelper.sendLifecycleReady()` — host에 transport lifecycle ready 신호 전송. 도메인 알림과는 채널이 다르다.
|
|
214
268
|
- `iframeHelper.debug.subscribe(handler)` — 디버그 스트림 구독
|
|
215
269
|
|
|
270
|
+
**Commands class prefix 컨벤션:**
|
|
271
|
+
|
|
272
|
+
- prefix 없는 prototype 메서드 → host에서 호출 가능한 remote command
|
|
273
|
+
- `_` prefix → 사용자 local-only (dispatch 제외)
|
|
274
|
+
- `$` prefix → 라이브러리 namespace (dispatch 제외, 인식되는 hook은 `$onCommandRun`)
|
|
275
|
+
|
|
276
|
+
`onStatusChange`처럼 prefix가 없으면서 host에서 호출하면 안 되는 메서드는 반드시 `_` prefix를 붙여야 한다. 그렇지 않으면 host가 `controller.invoke("onStatusChange", [...])`로 직접 호출할 수 있다.
|
|
277
|
+
|
|
216
278
|
> 알림은 iframe → host 단방향이다. host → iframe 알림은 라이브러리 외부에서 `postMessage`로 직접 처리하거나, host에서 커맨드를 호출해 처리한다.
|
|
217
279
|
|
|
218
280
|
### 공통 타입
|
|
@@ -223,9 +285,9 @@ host iframe
|
|
|
223
285
|
|
|
224
286
|
`targetOrigin`과 `allowedOrigins`는 반드시 명시적으로 지정한다. 와일드카드(`*`)는 사용하지 않는다.
|
|
225
287
|
|
|
226
|
-
| 옵션
|
|
227
|
-
|
|
228
|
-
| `targetOrigin`
|
|
288
|
+
| 옵션 | 의미 |
|
|
289
|
+
| ---------------- | ----------------------------------------- |
|
|
290
|
+
| `targetOrigin` | `postMessage` 전송 시 사용할 대상 origin |
|
|
229
291
|
| `allowedOrigins` | 수신 시 허용할 origin 화이트리스트 (배열) |
|
|
230
292
|
|
|
231
293
|
수신 메시지의 `event.origin`이 화이트리스트에 없으면 무시된다.
|
|
@@ -111,10 +111,12 @@ function isRecord(value) {
|
|
|
111
111
|
|
|
112
112
|
// src/core/transport.ts
|
|
113
113
|
function createIframeWindowTransport(iframe) {
|
|
114
|
+
var _a;
|
|
114
115
|
return {
|
|
115
|
-
expectedSource: iframe.contentWindow
|
|
116
|
+
expectedSource: (_a = iframe.contentWindow) != null ? _a : void 0,
|
|
116
117
|
post(message, targetOrigin, transfer) {
|
|
117
|
-
|
|
118
|
+
var _a2;
|
|
119
|
+
(_a2 = iframe.contentWindow) == null ? void 0 : _a2.postMessage(
|
|
118
120
|
message,
|
|
119
121
|
targetOrigin,
|
|
120
122
|
transfer
|
package/dist/host.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { H as HostDebugEvent, C as CommandMap, I as IframeCallControllerOptions, a as IframeCallController, S as SerializedIframeCallError } from './messages-
|
|
2
|
-
export { b as CommandArgs, c as CommandHandler, d as CommandResult, e as CommandRunner, f as IframeCallCallOptions, g as IframeCallLogger, h as IframeCallNotify, i as IframeCallRequest, j as IframeCallResponse, k as IframeCallTransferable, l as IframeCallTransport, m as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, n as ReservedNotificationName, o as createIframeCallError, p as createIframeCallErrorResponse, q as createIframeCallNotify, r as createIframeCallRequest, s as createIframeCallSuccessResponse, t as createIframeWindowTransport, u as isSerializedIframeCallError, v as parseIframeCallMessage, w as serializeIframeCallError } from './messages-
|
|
1
|
+
import { H as HostDebugEvent, C as CommandMap, I as IframeCallControllerOptions, a as IframeCallController, S as SerializedIframeCallError } from './messages-uOmcU6eT.js';
|
|
2
|
+
export { b as CommandArgs, c as CommandHandler, d as CommandResult, e as CommandRunner, f as IframeCallCallOptions, g as IframeCallLogger, h as IframeCallNotify, i as IframeCallRequest, j as IframeCallResponse, k as IframeCallTransferable, l as IframeCallTransport, m as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, n as ReservedNotificationName, o as createIframeCallError, p as createIframeCallErrorResponse, q as createIframeCallNotify, r as createIframeCallRequest, s as createIframeCallSuccessResponse, t as createIframeWindowTransport, u as isSerializedIframeCallError, v as parseIframeCallMessage, w as serializeIframeCallError } from './messages-uOmcU6eT.js';
|
|
3
3
|
|
|
4
4
|
/** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:host]가 기본값이다. */
|
|
5
5
|
type ConsoleDebugLoggerOptions = {
|
package/dist/host.js
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
isSerializedIframeCallError,
|
|
9
9
|
parseIframeCallMessage,
|
|
10
10
|
serializeIframeCallError
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RSUK22E2.js";
|
|
12
12
|
|
|
13
13
|
// src/host/consoleDebugLogger.ts
|
|
14
14
|
function consoleDebugLogger(options = {}) {
|
|
15
|
-
|
|
15
|
+
var _a;
|
|
16
|
+
const prefix = (_a = options.prefix) != null ? _a : "[iframecall:host]";
|
|
16
17
|
const head = prefix.length > 0 ? `${prefix} ` : "";
|
|
17
18
|
return (event) => {
|
|
18
19
|
switch (event.type) {
|
|
@@ -95,7 +96,7 @@ function createControllerLifecycle(options) {
|
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
cleanupDone = true;
|
|
98
|
-
onCleanup
|
|
99
|
+
onCleanup == null ? void 0 : onCleanup();
|
|
99
100
|
}
|
|
100
101
|
return {
|
|
101
102
|
ready,
|
|
@@ -117,7 +118,8 @@ function createNotifyHandlerRegistry() {
|
|
|
117
118
|
const handlers = /* @__PURE__ */ new Map();
|
|
118
119
|
return {
|
|
119
120
|
register(event, handler) {
|
|
120
|
-
|
|
121
|
+
var _a;
|
|
122
|
+
const set = (_a = handlers.get(event)) != null ? _a : /* @__PURE__ */ new Set();
|
|
121
123
|
set.add(handler);
|
|
122
124
|
handlers.set(event, set);
|
|
123
125
|
return () => {
|
|
@@ -203,7 +205,8 @@ function createPendingCallRegistry(transport, targetOrigin) {
|
|
|
203
205
|
pending.clear();
|
|
204
206
|
},
|
|
205
207
|
getCommand(id) {
|
|
206
|
-
|
|
208
|
+
var _a;
|
|
209
|
+
return (_a = pending.get(id)) == null ? void 0 : _a.command;
|
|
207
210
|
}
|
|
208
211
|
};
|
|
209
212
|
}
|
|
@@ -244,24 +247,26 @@ function createReadyQueue() {
|
|
|
244
247
|
|
|
245
248
|
// src/host/controller.ts
|
|
246
249
|
function createIframeCallController(options) {
|
|
250
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
247
251
|
const targetOrigin = requireTargetOrigin(options.targetOrigin);
|
|
248
|
-
const allowedOrigins = new Set(options.allowedOrigins
|
|
249
|
-
const transport = options.transport
|
|
250
|
-
const generateId = options.generateId
|
|
251
|
-
const defaultTimeoutMs = options.defaultTimeoutMs
|
|
252
|
-
const readyTimeoutMs = options.readyTimeoutMs
|
|
253
|
-
const readyPolicy = options.readyPolicy
|
|
254
|
-
const readyQueueLimit = options.readyQueueLimit
|
|
252
|
+
const allowedOrigins = new Set((_a = options.allowedOrigins) != null ? _a : [targetOrigin]);
|
|
253
|
+
const transport = (_b = options.transport) != null ? _b : createIframeWindowTransport(options.iframe);
|
|
254
|
+
const generateId = (_c = options.generateId) != null ? _c : createDefaultRequestId;
|
|
255
|
+
const defaultTimeoutMs = (_d = options.defaultTimeoutMs) != null ? _d : 3e4;
|
|
256
|
+
const readyTimeoutMs = (_e = options.readyTimeoutMs) != null ? _e : defaultTimeoutMs;
|
|
257
|
+
const readyPolicy = (_f = options.readyPolicy) != null ? _f : "queue";
|
|
258
|
+
const readyQueueLimit = (_g = options.readyQueueLimit) != null ? _g : Number.POSITIVE_INFINITY;
|
|
255
259
|
const pending = createPendingCallRegistry(transport, targetOrigin);
|
|
256
260
|
const queue = createReadyQueue();
|
|
257
261
|
const notifyRegistry = createNotifyHandlerRegistry();
|
|
258
262
|
const debugSubscribers = /* @__PURE__ */ new Set();
|
|
259
263
|
function emitDebug(event) {
|
|
264
|
+
var _a2;
|
|
260
265
|
for (const handler of debugSubscribers) {
|
|
261
266
|
try {
|
|
262
267
|
handler(event);
|
|
263
268
|
} catch (error) {
|
|
264
|
-
options.logger
|
|
269
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall debug subscriber threw.", error);
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
}
|
|
@@ -289,68 +294,67 @@ function createIframeCallController(options) {
|
|
|
289
294
|
unsubscribeTransport();
|
|
290
295
|
notifyRegistry.clear();
|
|
291
296
|
});
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
297
|
+
const invoke = (cmd, args, callOptions) => {
|
|
298
|
+
var _a2;
|
|
299
|
+
const terminatedError = lifecycle.getTerminatedError();
|
|
300
|
+
if (terminatedError !== null) {
|
|
301
|
+
return Promise.reject(terminatedError);
|
|
302
|
+
}
|
|
303
|
+
if (!lifecycle.isReady() && readyPolicy === "reject") {
|
|
304
|
+
return Promise.reject(
|
|
305
|
+
createIframeCallError("not_ready", "Iframe is not ready.", {
|
|
306
|
+
command: cmd
|
|
307
|
+
})
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
const id = generateId();
|
|
311
|
+
const timeoutMs = (_a2 = callOptions == null ? void 0 : callOptions.timeoutMs) != null ? _a2 : defaultTimeoutMs;
|
|
312
|
+
return new Promise((resolve, reject) => {
|
|
313
|
+
const timeoutId = timeoutMs === 0 || timeoutMs === Number.POSITIVE_INFINITY ? null : setTimeout(() => {
|
|
314
|
+
pending.delete(id);
|
|
315
|
+
queue.delete(id);
|
|
316
|
+
reject(
|
|
317
|
+
createIframeCallError("timeout", "Command timed out.", {
|
|
318
|
+
command: cmd,
|
|
319
|
+
details: { timeoutMs }
|
|
304
320
|
})
|
|
305
321
|
);
|
|
306
|
-
}
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
322
|
+
}, timeoutMs);
|
|
323
|
+
const call = {
|
|
324
|
+
command: cmd,
|
|
325
|
+
timeoutId,
|
|
326
|
+
resolve,
|
|
327
|
+
reject
|
|
328
|
+
};
|
|
329
|
+
if (!lifecycle.isReady()) {
|
|
330
|
+
if (queue.size() >= readyQueueLimit) {
|
|
331
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
313
332
|
reject(
|
|
314
|
-
createIframeCallError("
|
|
333
|
+
createIframeCallError("queue_overflow", "Ready queue overflow.", {
|
|
315
334
|
command: cmd,
|
|
316
|
-
details: {
|
|
335
|
+
details: { readyQueueLimit }
|
|
317
336
|
})
|
|
318
337
|
);
|
|
319
|
-
}, timeoutMs);
|
|
320
|
-
const call = {
|
|
321
|
-
command: cmd,
|
|
322
|
-
timeoutId,
|
|
323
|
-
resolve,
|
|
324
|
-
reject
|
|
325
|
-
};
|
|
326
|
-
if (!lifecycle.isReady()) {
|
|
327
|
-
if (queue.size() >= readyQueueLimit) {
|
|
328
|
-
if (timeoutId !== null) {
|
|
329
|
-
clearTimeout(timeoutId);
|
|
330
|
-
}
|
|
331
|
-
reject(
|
|
332
|
-
createIframeCallError(
|
|
333
|
-
"queue_overflow",
|
|
334
|
-
"Ready queue overflow.",
|
|
335
|
-
{
|
|
336
|
-
command: cmd,
|
|
337
|
-
details: { readyQueueLimit }
|
|
338
|
-
}
|
|
339
|
-
)
|
|
340
|
-
);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
queue.add(id, {
|
|
344
|
-
...call,
|
|
345
|
-
args,
|
|
346
|
-
transfer: callOptions?.transfer
|
|
347
|
-
});
|
|
348
338
|
return;
|
|
349
339
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
340
|
+
queue.add(id, {
|
|
341
|
+
...call,
|
|
342
|
+
args,
|
|
343
|
+
transfer: callOptions == null ? void 0 : callOptions.transfer
|
|
344
|
+
});
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
emitDebug({ type: "commandSentToIframe", command: cmd, args });
|
|
348
|
+
pending.add(id, call);
|
|
349
|
+
pending.post(id, cmd, args, callOptions == null ? void 0 : callOptions.transfer);
|
|
350
|
+
});
|
|
351
|
+
};
|
|
352
|
+
const controller = {
|
|
353
|
+
ready: lifecycle.ready,
|
|
354
|
+
terminated: lifecycle.terminated,
|
|
355
|
+
invoke,
|
|
356
|
+
call(cmd, args, callOptions) {
|
|
357
|
+
return invoke(cmd, args, callOptions);
|
|
354
358
|
},
|
|
355
359
|
onNotificationFromIframe(event, handler) {
|
|
356
360
|
return notifyRegistry.register(
|
|
@@ -367,6 +371,7 @@ function createIframeCallController(options) {
|
|
|
367
371
|
}
|
|
368
372
|
},
|
|
369
373
|
async dispose(reason = "host_requested") {
|
|
374
|
+
var _a2;
|
|
370
375
|
if (lifecycle.isTerminated()) {
|
|
371
376
|
lifecycle.cleanup();
|
|
372
377
|
return;
|
|
@@ -384,7 +389,7 @@ function createIframeCallController(options) {
|
|
|
384
389
|
targetOrigin
|
|
385
390
|
);
|
|
386
391
|
} catch (error) {
|
|
387
|
-
options.logger
|
|
392
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall dispose message failed.", error);
|
|
388
393
|
} finally {
|
|
389
394
|
lifecycle.terminate(lifecycleError);
|
|
390
395
|
lifecycle.cleanup();
|
|
@@ -393,6 +398,14 @@ function createIframeCallController(options) {
|
|
|
393
398
|
};
|
|
394
399
|
return controller;
|
|
395
400
|
}
|
|
401
|
+
function createDefaultRequestId() {
|
|
402
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
403
|
+
let id = "";
|
|
404
|
+
for (const byte of bytes) {
|
|
405
|
+
id += byte.toString(16).padStart(2, "0");
|
|
406
|
+
}
|
|
407
|
+
return id;
|
|
408
|
+
}
|
|
396
409
|
function createTransportRouter(deps) {
|
|
397
410
|
const {
|
|
398
411
|
lifecycle,
|
|
@@ -411,7 +424,7 @@ function createTransportRouter(deps) {
|
|
|
411
424
|
return;
|
|
412
425
|
}
|
|
413
426
|
const parsed = parseIframeCallMessage(event.data);
|
|
414
|
-
if (parsed
|
|
427
|
+
if ((parsed == null ? void 0 : parsed.type) === "response") {
|
|
415
428
|
const responseMessage = parsed.message;
|
|
416
429
|
const command = pending.getCommand(responseMessage.id);
|
|
417
430
|
pending.settle(responseMessage.id, responseMessage);
|
|
@@ -432,7 +445,7 @@ function createTransportRouter(deps) {
|
|
|
432
445
|
}
|
|
433
446
|
return;
|
|
434
447
|
}
|
|
435
|
-
if (parsed
|
|
448
|
+
if ((parsed == null ? void 0 : parsed.type) !== "notify") return;
|
|
436
449
|
const { event: notifyEvent, payload } = parsed.message;
|
|
437
450
|
if (notifyEvent === "ready") {
|
|
438
451
|
handleReadyNotify(payload, lifecycle, queue, pending, logger, emitDebug);
|
|
@@ -445,7 +458,7 @@ function createTransportRouter(deps) {
|
|
|
445
458
|
emitDebug({
|
|
446
459
|
type: "terminatedReceived",
|
|
447
460
|
reason,
|
|
448
|
-
error: cause
|
|
461
|
+
error: cause != null ? cause : null
|
|
449
462
|
});
|
|
450
463
|
lifecycle.terminate(
|
|
451
464
|
createIframeCallError("terminated", "Iframe terminated.", {
|
|
@@ -465,7 +478,7 @@ function createTransportRouter(deps) {
|
|
|
465
478
|
}
|
|
466
479
|
function handleReadyNotify(payload, lifecycle, queue, pending, logger, emitDebug) {
|
|
467
480
|
if (lifecycle.isReady()) {
|
|
468
|
-
logger
|
|
481
|
+
logger == null ? void 0 : logger.warn("iframecall duplicate ready ignored.", payload);
|
|
469
482
|
return;
|
|
470
483
|
}
|
|
471
484
|
if (!isSupportedReadyPayload(payload)) {
|
package/dist/iframe.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { x as IframeDebugEvent, C as CommandMap, y as IframeCallRunnerOptions, z as IframeCallRunnerHandle, A as CommandsConstructor, B as IframeCallRunnerClassOptions, D as IframeHelper } from './messages-
|
|
2
|
-
export { b as CommandArgs, c as CommandHandler, d as CommandResult, e as CommandRunner, E as DomainNotificationKey, f as IframeCallCallOptions, g as IframeCallLogger, h as IframeCallNotify, i as IframeCallRequest, j as IframeCallResponse, k as IframeCallTransferable, l as IframeCallTransport, m as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, n as ReservedNotificationName, S as SerializedIframeCallError, o as createIframeCallError, p as createIframeCallErrorResponse, q as createIframeCallNotify, r as createIframeCallRequest, s as createIframeCallSuccessResponse, F as createParentWindowTransport, u as isSerializedIframeCallError, v as parseIframeCallMessage, w as serializeIframeCallError } from './messages-
|
|
1
|
+
import { x as IframeDebugEvent, C as CommandMap, y as IframeCallRunnerOptions, z as IframeCallRunnerHandle, A as CommandsConstructor, B as IframeCallRunnerClassOptions, D as IframeHelper } from './messages-uOmcU6eT.js';
|
|
2
|
+
export { b as CommandArgs, c as CommandHandler, d as CommandResult, e as CommandRunner, E as DomainNotificationKey, f as IframeCallCallOptions, g as IframeCallLogger, h as IframeCallNotify, i as IframeCallRequest, j as IframeCallResponse, k as IframeCallTransferable, l as IframeCallTransport, m as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, n as ReservedNotificationName, S as SerializedIframeCallError, o as createIframeCallError, p as createIframeCallErrorResponse, q as createIframeCallNotify, r as createIframeCallRequest, s as createIframeCallSuccessResponse, F as createParentWindowTransport, u as isSerializedIframeCallError, v as parseIframeCallMessage, w as serializeIframeCallError } from './messages-uOmcU6eT.js';
|
|
3
3
|
|
|
4
4
|
/** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:iframe]가 기본값이다. */
|
|
5
5
|
type ConsoleDebugLoggerOptions = {
|
|
@@ -40,15 +40,15 @@ type UseIframeCallRunnerOptions<TCommands, TNotificationsToHost = Record<string,
|
|
|
40
40
|
} & Pick<IframeCallRunnerClassOptions<TCommands, TNotificationsToHost>, "logger" | "onHostDispose" | "transport">;
|
|
41
41
|
/**
|
|
42
42
|
* useIframeCallRunner가 반환하는 handle.
|
|
43
|
-
* mount 전에는
|
|
43
|
+
* mount 전에는 commands/iframeHelper가 null이며, mount 후 runner가 생성되면 채워진다.
|
|
44
|
+
* runner handle 전체는 더 이상 노출하지 않고 도메인 코드가 필요로 하는
|
|
45
|
+
* commands 인스턴스와 iframeHelper만 외부로 흘려보낸다.
|
|
44
46
|
*/
|
|
45
47
|
type UseIframeCallRunnerResult<TCommands, TNotificationsToHost = Record<string, unknown>> = {
|
|
46
|
-
/** runner가 생성한 Commands 인스턴스. mount 전
|
|
47
|
-
readonly commands: TCommands |
|
|
48
|
-
/** Commands constructor에 주입된 iframeHelper. mount 전
|
|
49
|
-
readonly iframeHelper: IframeHelper<TNotificationsToHost> |
|
|
50
|
-
/** runner handle 전체. mount 전 undefined. */
|
|
51
|
-
readonly runner: IframeCallRunnerHandle<TCommands, TNotificationsToHost> | undefined;
|
|
48
|
+
/** runner가 생성한 Commands 인스턴스. mount 전 null. */
|
|
49
|
+
readonly commands: TCommands | null;
|
|
50
|
+
/** Commands constructor에 주입된 iframeHelper. mount 전 null. */
|
|
51
|
+
readonly iframeHelper: IframeHelper<TNotificationsToHost> | null;
|
|
52
52
|
/** runner가 활성화되어 있으면 true. mount 전 false. */
|
|
53
53
|
readonly isActive: boolean;
|
|
54
54
|
};
|
package/dist/iframe.js
CHANGED
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
isSerializedIframeCallError,
|
|
9
9
|
parseIframeCallMessage,
|
|
10
10
|
serializeIframeCallError
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RSUK22E2.js";
|
|
12
12
|
|
|
13
13
|
// src/iframe/consoleDebugLogger.ts
|
|
14
14
|
function consoleDebugLogger(options = {}) {
|
|
15
|
-
|
|
15
|
+
var _a;
|
|
16
|
+
const prefix = (_a = options.prefix) != null ? _a : "[iframecall:iframe]";
|
|
16
17
|
const head = prefix.length > 0 ? `${prefix} ` : "";
|
|
17
18
|
return (event) => {
|
|
18
19
|
switch (event.type) {
|
|
@@ -38,18 +39,20 @@ function consoleDebugLogger(options = {}) {
|
|
|
38
39
|
// src/iframe/runner.ts
|
|
39
40
|
var RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set(["constructor", "host:dispose"]);
|
|
40
41
|
function createIframeCallRunner(options) {
|
|
42
|
+
var _a, _b;
|
|
41
43
|
const targetOrigin = requireTargetOrigin(options.targetOrigin);
|
|
42
|
-
const allowedOrigins = new Set(options.allowedOrigins
|
|
43
|
-
const transport = options.transport
|
|
44
|
+
const allowedOrigins = new Set((_a = options.allowedOrigins) != null ? _a : [targetOrigin]);
|
|
45
|
+
const transport = (_b = options.transport) != null ? _b : createParentWindowTransport();
|
|
44
46
|
let disposing = false;
|
|
45
47
|
let disposed = false;
|
|
46
48
|
const debugSubscribers = /* @__PURE__ */ new Set();
|
|
47
49
|
function emitDebug(event) {
|
|
50
|
+
var _a2;
|
|
48
51
|
for (const handler of debugSubscribers) {
|
|
49
52
|
try {
|
|
50
53
|
handler(event);
|
|
51
54
|
} catch (error) {
|
|
52
|
-
options.logger
|
|
55
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall debug subscriber threw.", error);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -59,7 +62,7 @@ function createIframeCallRunner(options) {
|
|
|
59
62
|
safePost(createIframeCallNotify(event, payload));
|
|
60
63
|
emitDebug({ type: "notificationSentToHost", event, payload });
|
|
61
64
|
},
|
|
62
|
-
|
|
65
|
+
sendLifecycleReady() {
|
|
63
66
|
if (disposing || disposed) return;
|
|
64
67
|
safePost(createIframeCallNotify("ready", { protocolVersion: 1 }));
|
|
65
68
|
},
|
|
@@ -73,7 +76,7 @@ function createIframeCallRunner(options) {
|
|
|
73
76
|
}
|
|
74
77
|
};
|
|
75
78
|
const iframeHelper = helperInternal;
|
|
76
|
-
const { commands, dispatch } = resolveCommandSource(options, iframeHelper);
|
|
79
|
+
const { commands, dispatch, commandRunHook } = resolveCommandSource(options, iframeHelper);
|
|
77
80
|
const unsubscribeTransport = transport.subscribe((event) => {
|
|
78
81
|
if (disposing || disposed) return;
|
|
79
82
|
if (!allowedOrigins.has(event.origin)) {
|
|
@@ -83,7 +86,7 @@ function createIframeCallRunner(options) {
|
|
|
83
86
|
return;
|
|
84
87
|
}
|
|
85
88
|
const parsed = parseIframeCallMessage(event.data);
|
|
86
|
-
if (parsed
|
|
89
|
+
if ((parsed == null ? void 0 : parsed.type) === "notify") {
|
|
87
90
|
emitDebug({
|
|
88
91
|
type: "notificationReceivedFromHost",
|
|
89
92
|
event: parsed.message.event,
|
|
@@ -91,7 +94,7 @@ function createIframeCallRunner(options) {
|
|
|
91
94
|
});
|
|
92
95
|
return;
|
|
93
96
|
}
|
|
94
|
-
if (parsed
|
|
97
|
+
if ((parsed == null ? void 0 : parsed.type) !== "request") {
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
97
100
|
if (parsed.message.cmd === "host:dispose") {
|
|
@@ -105,19 +108,21 @@ function createIframeCallRunner(options) {
|
|
|
105
108
|
);
|
|
106
109
|
});
|
|
107
110
|
function safePost(message, transfer) {
|
|
111
|
+
var _a2;
|
|
108
112
|
try {
|
|
109
113
|
transport.post(message, targetOrigin, transfer);
|
|
110
114
|
} catch (error) {
|
|
111
|
-
options.logger
|
|
115
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall postMessage failed.", error);
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
async function handleHostDispose(reason) {
|
|
119
|
+
var _a2, _b2;
|
|
115
120
|
if (disposing || disposed) return;
|
|
116
121
|
disposing = true;
|
|
117
122
|
try {
|
|
118
|
-
await options.onHostDispose
|
|
123
|
+
await ((_a2 = options.onHostDispose) == null ? void 0 : _a2.call(options, reason));
|
|
119
124
|
} catch (error) {
|
|
120
|
-
options.logger
|
|
125
|
+
(_b2 = options.logger) == null ? void 0 : _b2.warn("iframecall host dispose handler failed.", error);
|
|
121
126
|
} finally {
|
|
122
127
|
safePost(createIframeCallNotify("terminated", { reason }));
|
|
123
128
|
disposed = true;
|
|
@@ -139,7 +144,8 @@ function createIframeCallRunner(options) {
|
|
|
139
144
|
return;
|
|
140
145
|
}
|
|
141
146
|
try {
|
|
142
|
-
const
|
|
147
|
+
const invoke = () => Promise.resolve(handler(...args));
|
|
148
|
+
const value = commandRunHook ? await commandRunHook(cmd, args, invoke) : await invoke();
|
|
143
149
|
if (disposing || disposed) return;
|
|
144
150
|
safePost(createIframeCallSuccessResponse(id, value));
|
|
145
151
|
emitDebug({ type: "commandResultSentToHost", command: cmd, value });
|
|
@@ -159,8 +165,8 @@ function createIframeCallRunner(options) {
|
|
|
159
165
|
commands,
|
|
160
166
|
iframeHelper,
|
|
161
167
|
sendNotificationToHost: sendNotificationToHostUntyped,
|
|
162
|
-
|
|
163
|
-
iframeHelper.
|
|
168
|
+
sendLifecycleReady() {
|
|
169
|
+
iframeHelper.sendLifecycleReady();
|
|
164
170
|
},
|
|
165
171
|
terminated(reason, error) {
|
|
166
172
|
if (disposing || disposed) return;
|
|
@@ -197,11 +203,13 @@ function resolveCommandSource(options, iframeHelper) {
|
|
|
197
203
|
const Ctor = rawOptions.Commands;
|
|
198
204
|
const instance = new Ctor(iframeHelper);
|
|
199
205
|
const handlerCache = buildPrototypeCommandHandlers(instance);
|
|
206
|
+
const commandRunHook = resolveCommandRunHook(instance);
|
|
200
207
|
return {
|
|
201
208
|
commands: instance,
|
|
202
209
|
dispatch(cmd) {
|
|
203
210
|
return handlerCache.get(cmd);
|
|
204
|
-
}
|
|
211
|
+
},
|
|
212
|
+
commandRunHook
|
|
205
213
|
};
|
|
206
214
|
}
|
|
207
215
|
function buildPrototypeCommandHandlers(instance) {
|
|
@@ -212,6 +220,7 @@ function buildPrototypeCommandHandlers(instance) {
|
|
|
212
220
|
if (typeof key !== "string") continue;
|
|
213
221
|
if (RESERVED_COMMAND_NAMES.has(key)) continue;
|
|
214
222
|
if (key.startsWith("_")) continue;
|
|
223
|
+
if (key.startsWith("$")) continue;
|
|
215
224
|
if (handlers.has(key)) continue;
|
|
216
225
|
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
217
226
|
if (descriptor === void 0) continue;
|
|
@@ -225,6 +234,18 @@ function buildPrototypeCommandHandlers(instance) {
|
|
|
225
234
|
}
|
|
226
235
|
return handlers;
|
|
227
236
|
}
|
|
237
|
+
function resolveCommandRunHook(instance) {
|
|
238
|
+
let proto = Object.getPrototypeOf(instance);
|
|
239
|
+
while (proto !== null && proto !== Object.prototype) {
|
|
240
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "$onCommandRun");
|
|
241
|
+
if (descriptor !== void 0 && typeof descriptor.value === "function") {
|
|
242
|
+
const hook = descriptor.value;
|
|
243
|
+
return hook.bind(instance);
|
|
244
|
+
}
|
|
245
|
+
proto = Object.getPrototypeOf(proto);
|
|
246
|
+
}
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
228
249
|
function requireTargetOrigin(targetOrigin) {
|
|
229
250
|
if (targetOrigin.length === 0 || targetOrigin === "*" || targetOrigin === "null") {
|
|
230
251
|
throw createIframeCallError(
|
|
@@ -244,6 +265,7 @@ function getDisposeReason(payload) {
|
|
|
244
265
|
// src/iframe/useIframeCallRunner.tsx
|
|
245
266
|
import { useEffect, useRef, useState } from "react";
|
|
246
267
|
function useIframeCallRunner(options) {
|
|
268
|
+
var _a, _b;
|
|
247
269
|
const optionsRef = useRef(options);
|
|
248
270
|
optionsRef.current = options;
|
|
249
271
|
const runnerRef = useRef(null);
|
|
@@ -277,9 +299,8 @@ function useIframeCallRunner(options) {
|
|
|
277
299
|
}, []);
|
|
278
300
|
const runner = runnerRef.current;
|
|
279
301
|
return {
|
|
280
|
-
commands: runner
|
|
281
|
-
iframeHelper: runner
|
|
282
|
-
runner: runner ?? void 0,
|
|
302
|
+
commands: (_a = runner == null ? void 0 : runner.commands) != null ? _a : null,
|
|
303
|
+
iframeHelper: (_b = runner == null ? void 0 : runner.iframeHelper) != null ? _b : null,
|
|
283
304
|
isActive
|
|
284
305
|
};
|
|
285
306
|
}
|
|
@@ -107,7 +107,7 @@ type IframeCallControllerOptions<TCommands extends CommandMap<TCommands>> = {
|
|
|
107
107
|
readonly readyPolicy?: ReadyPolicy;
|
|
108
108
|
/** queue 정책일 때 대기열 최대 크기. 초과 호출은 queue_overflow 에러로 거부한다. */
|
|
109
109
|
readonly readyQueueLimit?: number;
|
|
110
|
-
/**
|
|
110
|
+
/** invoke() 호출별 timeout 기본값(ms). 0 또는 Infinity면 timeout을 적용하지 않는다. */
|
|
111
111
|
readonly defaultTimeoutMs?: number;
|
|
112
112
|
/** ready 신호 대기 timeout(ms). 미지정이면 defaultTimeoutMs를 따른다. */
|
|
113
113
|
readonly readyTimeoutMs?: number;
|
|
@@ -141,7 +141,7 @@ type IframeHelper<TNotificationsToHost = Record<string, unknown>> = {
|
|
|
141
141
|
/** 도메인 notification을 host로 전송한다. lifecycle 예약 이름은 받지 않는다. */
|
|
142
142
|
sendNotificationToHost: IsWildcardNotificationMap<TNotificationsToHost> extends true ? (event: string, payload: unknown) => void : <K extends DomainNotificationKey<TNotificationsToHost>>(event: K, payload: TNotificationsToHost[K]) => void;
|
|
143
143
|
/** transport lifecycle ready 신호를 host로 전송한다. payload는 라이브러리가 고정한다. */
|
|
144
|
-
|
|
144
|
+
sendLifecycleReady(): void;
|
|
145
145
|
/** 개발/디버그 패널이 iframecall 통신 흐름을 관찰할 수 있도록 raw event를 흘려준다. */
|
|
146
146
|
readonly debug: {
|
|
147
147
|
subscribe(handler: (event: IframeDebugEvent) => void): () => void;
|
|
@@ -230,10 +230,12 @@ type IframeCallController<TCommands extends CommandMap<TCommands>, TNotification
|
|
|
230
230
|
/** 종료 사유를 노출하는 promise. 정상 dispose면 null, 비정상 종료면 직렬화된 에러로 resolve된다. */
|
|
231
231
|
readonly terminated: Promise<SerializedIframeCallError | null>;
|
|
232
232
|
/**
|
|
233
|
-
* iframe에 등록된 command를
|
|
234
|
-
* ready 이전
|
|
235
|
-
* timeout/transfer는
|
|
233
|
+
* iframe에 등록된 command를 실행한다.
|
|
234
|
+
* ready 이전 실행은 readyPolicy에 따라 queue되거나 즉시 거부된다.
|
|
235
|
+
* timeout/transfer는 실행별 options로 지정한다.
|
|
236
236
|
*/
|
|
237
|
+
invoke<K extends keyof TCommands & string>(cmd: K, args: CommandArgs<TCommands[K]>, options?: IframeCallCallOptions): Promise<CommandResult<TCommands[K]>>;
|
|
238
|
+
/** @deprecated `invoke()`를 사용한다. 다음 major release에서 제거될 예정이다. */
|
|
237
239
|
call<K extends keyof TCommands & string>(cmd: K, args: CommandArgs<TCommands[K]>, options?: IframeCallCallOptions): Promise<CommandResult<TCommands[K]>>;
|
|
238
240
|
/**
|
|
239
241
|
* iframe이 host로 보낸 notification을 event 이름과 payload로 구독한다.
|
|
@@ -272,7 +274,7 @@ type IframeCallRunnerHandle<TCommands = Record<string, CommandHandler>, TNotific
|
|
|
272
274
|
/** iframeHelper.sendNotificationToHost와 동일한 동작을 runner handle에서 노출한다. */
|
|
273
275
|
sendNotificationToHost: IsWildcardNotificationMap<TNotificationsToHost> extends true ? (event: string, payload: unknown) => void : <K extends DomainNotificationKey<TNotificationsToHost>>(event: K, payload: TNotificationsToHost[K]) => void;
|
|
274
276
|
/** transport lifecycle ready 신호. payload는 `{ protocolVersion: 1 }`로 고정된다. */
|
|
275
|
-
|
|
277
|
+
sendLifecycleReady(): void;
|
|
276
278
|
terminated(reason: string, error?: SerializedIframeCallError): void;
|
|
277
279
|
/** local transport subscription을 즉시 정리하고 이후 inbound/outbound를 모두 no-op으로 만든다. */
|
|
278
280
|
dispose(reason?: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cp949/iframecall",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "타입 안전한 host ↔ iframe postMessage 호출 라이브러리 (React 18/19 지원)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iframe",
|
|
@@ -24,9 +24,14 @@
|
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"sideEffects": false,
|
|
27
|
+
"browserslist": [
|
|
28
|
+
"chrome 75",
|
|
29
|
+
"firefox 67"
|
|
30
|
+
],
|
|
27
31
|
"files": [
|
|
28
32
|
"dist",
|
|
29
33
|
"README.md",
|
|
34
|
+
"CHANGELOG.md",
|
|
30
35
|
"LICENSE"
|
|
31
36
|
],
|
|
32
37
|
"exports": {
|
|
@@ -42,25 +47,29 @@
|
|
|
42
47
|
"publishConfig": {
|
|
43
48
|
"access": "public"
|
|
44
49
|
},
|
|
45
|
-
"peerDependencies": {
|
|
46
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
47
|
-
"@types/react": "^18.0.0 || ^19.0.0"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@testing-library/dom": "^10.4.0",
|
|
51
|
-
"@testing-library/react": "^16.3.0",
|
|
52
|
-
"@types/react": "19.2.2",
|
|
53
|
-
"jsdom": "^25.0.1",
|
|
54
|
-
"react": "^19.2.0",
|
|
55
|
-
"typescript": "6.0.3",
|
|
56
|
-
"tsup": "^8.0.0",
|
|
57
|
-
"vitest": "^4.1.5",
|
|
58
|
-
"@repo/typescript-config": "0.0.0"
|
|
59
|
-
},
|
|
60
50
|
"scripts": {
|
|
61
51
|
"build": "tsup",
|
|
62
52
|
"dev": "tsup --watch",
|
|
53
|
+
"lint": "eslint . --max-warnings 0",
|
|
63
54
|
"check-types": "tsc --noEmit",
|
|
64
|
-
"test": "vitest run tests/unit --passWithNoTests"
|
|
55
|
+
"test": "vitest run tests/unit --passWithNoTests",
|
|
56
|
+
"prepublishOnly": "pnpm run build"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
60
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@repo/eslint-config": "workspace:*",
|
|
64
|
+
"@repo/typescript-config": "workspace:*",
|
|
65
|
+
"@testing-library/dom": "^10.4.1",
|
|
66
|
+
"@testing-library/react": "^16.3.2",
|
|
67
|
+
"@types/react": "19.2.18",
|
|
68
|
+
"eslint": "^9.39.5",
|
|
69
|
+
"jsdom": "^29.1.1",
|
|
70
|
+
"react": "^19.2.8",
|
|
71
|
+
"tsup": "^8.5.1",
|
|
72
|
+
"typescript": "6.0.3",
|
|
73
|
+
"vitest": "^4.1.11"
|
|
65
74
|
}
|
|
66
|
-
}
|
|
75
|
+
}
|