@cp949/iframecall 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +93 -41
- package/dist/{chunk-UYZXYOI6.js → chunk-UNM5VKFB.js} +24 -3
- package/dist/host.d.ts +5 -6
- package/dist/host.js +234 -194
- package/dist/iframe.d.ts +2 -2
- package/dist/iframe.js +43 -22
- package/dist/{messages-BuRipoIx.d.ts → messages-Bk4RCDOd.d.ts} +33 -10
- package/package.json +18 -11
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-09-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- host controller와 `useIframeCallController`에 `opaqueOrigin: true` 옵션을 추가했습니다. `sandbox`로 격리된 opaque origin(`"null"`) iframe과 통신합니다. 송신은 `"*"`, 수신은 origin `"null"`과 `contentWindow` source가 모두 일치할 때만 받습니다.
|
|
14
|
+
- `IframeCallControllerOriginOptions` 타입을 export합니다.
|
|
15
|
+
- ready 재요청 handshake를 추가했습니다. host controller는 구독 직후 `host:ready-query` notify를 보내고, 이미 ready를 보낸 runner는 `requested: true` ready로 다시 응답합니다. iframe이 host보다 먼저 로드되어 첫 ready가 유실되던 문제(controller가 `pending`에 머묾)를 해결합니다. 이전 버전 host/runner와 섞여도 동작은 기존과 같습니다(`protocolVersion` 1 유지).
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- host controller가 생성 시 `host:ready-query` notify를 한 번 전송합니다. `requested: true`가 붙은 중복 ready는 경고 없이 무시합니다.
|
|
20
|
+
- `opaqueOrigin` 모드에서 `targetOrigin`/`allowedOrigins`를 함께 지정하거나 transport에 `expectedSource`가 없으면 `invalid_origin`을 던집니다. 기존 모드의 wildcard 거부 동작은 그대로입니다.
|
|
21
|
+
|
|
22
|
+
## [0.2.1] - 2026-08-25
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- host controller의 정식 command 실행 API로 `invoke(command, args, options?)`를 추가했습니다.
|
|
27
|
+
|
|
28
|
+
### Deprecated
|
|
29
|
+
|
|
30
|
+
- `controller.call()`은 호환성을 위해 유지하지만 다음 major release에서 제거할 예정입니다. `controller.invoke()`를 사용하세요.
|
|
31
|
+
|
|
7
32
|
## [0.2.0] - 2026-04-28
|
|
8
33
|
|
|
9
34
|
### Added
|
package/README.md
CHANGED
|
@@ -20,19 +20,19 @@ 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
|
+
host와 iframe은 서로 다른 origin에서도 동작하며, 각 진입점은 그쪽에서만 필요한 API와 타입만 노출한다.
|
|
36
36
|
|
|
37
37
|
## 빠른 시작
|
|
38
38
|
|
|
@@ -84,7 +84,7 @@ export function HostPage() {
|
|
|
84
84
|
|
|
85
85
|
const handleGreet = async () => {
|
|
86
86
|
if (!controller) return;
|
|
87
|
-
const result = await controller.
|
|
87
|
+
const result = await controller.invoke("greet", ["World"]);
|
|
88
88
|
console.log(result); // "Hello, World!"
|
|
89
89
|
};
|
|
90
90
|
|
|
@@ -94,11 +94,7 @@ export function HostPage() {
|
|
|
94
94
|
<button type="button" onClick={handleGreet} disabled={status !== "ready"}>
|
|
95
95
|
Greet
|
|
96
96
|
</button>
|
|
97
|
-
<iframe
|
|
98
|
-
ref={iframeRef}
|
|
99
|
-
src={IFRAME_ORIGIN}
|
|
100
|
-
title="iframe demo"
|
|
101
|
-
/>
|
|
97
|
+
<iframe ref={iframeRef} src={IFRAME_ORIGIN} title="iframe demo" />
|
|
102
98
|
</div>
|
|
103
99
|
);
|
|
104
100
|
}
|
|
@@ -110,11 +106,11 @@ export function HostPage() {
|
|
|
110
106
|
|
|
111
107
|
prefix 컨벤션:
|
|
112
108
|
|
|
113
|
-
| prefix | 의미
|
|
114
|
-
|
|
115
|
-
| (없음) | host로 dispatch되는 remote command
|
|
116
|
-
| `_`
|
|
117
|
-
| `$`
|
|
109
|
+
| prefix | 의미 |
|
|
110
|
+
| ------ | ---------------------------------------------------------------------------------------------------- |
|
|
111
|
+
| (없음) | host로 dispatch되는 remote command |
|
|
112
|
+
| `_` | 사용자 local-only 메서드. dispatch 대상에서 제외된다. (예: `_sendLifecycleReady`, `_onStatusChange`) |
|
|
113
|
+
| `$` | 라이브러리 점유 namespace. dispatch에서 제외되며, 라이브러리가 정의한 hook 이름만 의미가 있다. |
|
|
118
114
|
|
|
119
115
|
현재 라이브러리가 인식하는 hook은 한 개:
|
|
120
116
|
|
|
@@ -216,13 +212,15 @@ export function IframePage() {
|
|
|
216
212
|
```text
|
|
217
213
|
host iframe
|
|
218
214
|
│ │
|
|
215
|
+
│ controller 생성 (listener 등록) │
|
|
216
|
+
│ ── notify host:ready-query ─────────▶│ (ready 전이면 무시)
|
|
219
217
|
│ <iframe src="..."> │
|
|
220
218
|
│──────────────────────────────────────▶│ mount
|
|
221
219
|
│ │ commands._sendLifecycleReady() → sendLifecycleReady()
|
|
222
220
|
│ ◀── ready ───────────────────────────│ (lifecycle 채널)
|
|
223
221
|
│ controller.status = "ready" │
|
|
224
222
|
│ │
|
|
225
|
-
│ controller.
|
|
223
|
+
│ controller.invoke("greet", ["World"])│
|
|
226
224
|
│ ── request ─────────────────────────▶│ $onCommandRun → AppCommands.greet("World")
|
|
227
225
|
│ ◀── notify status-changed:processing │ (도메인 채널, refcount 0→1)
|
|
228
226
|
│ ◀── response: "Hello, World!" ───────│
|
|
@@ -230,7 +228,9 @@ host iframe
|
|
|
230
228
|
```
|
|
231
229
|
|
|
232
230
|
- iframe이 마운트되면 `commands._sendLifecycleReady()`가 `sendLifecycleReady()`를 통해 transport ready 신호를 보낸다.
|
|
233
|
-
-
|
|
231
|
+
- ready 전에 호출한 `controller.invoke`는 기본값(`readyPolicy: "queue"`)에서 ready까지 대기한 뒤 전송된다. `readyPolicy: "reject"`면 즉시 거부된다.
|
|
232
|
+
- **ready 재요청(ready-query)**: controller는 `message` listener를 등록한 직후 iframe에 `host:ready-query` notify를 한 번 보낸다. 이미 `sendLifecycleReady()`를 호출한 runner는 ready(`requested: true`)로 다시 응답한다. 그래서 SSR된 `<iframe src>`처럼 iframe이 host hydration보다 먼저 로드되어 첫 ready가 유실돼도 controller가 ready에 도달한다. 앱이 아직 ready 전이면 runner는 query를 무시하고, 이후 앱이 보내는 ready로 연결된다.
|
|
233
|
+
- host와 runner가 모두 이 기능을 포함한 버전일 때만 복구된다. 이전 버전 runner는 query를 무시하므로, 그 경우에는 iframe `src`를 controller가 생긴 뒤 설정한다(`src={controller ? IFRAME_URL : undefined}`). 같은 요소의 `src` 변경은 `contentWindow` identity를 유지하므로 source 검사에 영향이 없다.
|
|
234
234
|
- 응답은 Promise로 돌아오며, iframe 측 메서드가 throw하면 host 쪽 Promise는 reject된다.
|
|
235
235
|
- iframe → host 단방향 알림은 `sendNotificationToHost`로 보내고, host 쪽에서 `controller.onNotificationFromIframe`으로 받는다.
|
|
236
236
|
- **라이프사이클 채널과 도메인 채널은 책임이 다르다.** `ready`/`terminated`는 transport 신호 전용이고, 도메인 알림(`status-changed` 등)에는 `"ready"` 같은 lifecycle 의미를 담지 않는다.
|
|
@@ -240,16 +240,17 @@ host iframe
|
|
|
240
240
|
|
|
241
241
|
### host 진입점
|
|
242
242
|
|
|
243
|
-
| export
|
|
244
|
-
|
|
245
|
-
| `useIframeCallController`
|
|
246
|
-
| `createIframeCallController`
|
|
247
|
-
| `createIframeWindowTransport` | factory | 커스텀 트랜스포트 구성용
|
|
248
|
-
| `consoleDebugLogger`
|
|
243
|
+
| export | 종류 | 설명 |
|
|
244
|
+
| ----------------------------- | ------- | -------------------------------------------------------------------------------------------------- |
|
|
245
|
+
| `useIframeCallController` | hook | host용 React 훅. `iframeRef`, `controller`, `status`, `terminationError`, `readyError`를 반환한다. |
|
|
246
|
+
| `createIframeCallController` | factory | 훅 없이 컨트롤러를 직접 만들 때 사용 |
|
|
247
|
+
| `createIframeWindowTransport` | factory | 커스텀 트랜스포트 구성용 |
|
|
248
|
+
| `consoleDebugLogger` | util | 디버그 이벤트를 콘솔에 출력하는 로거 |
|
|
249
249
|
|
|
250
250
|
훅이 반환하는 `controller`의 주요 멤버:
|
|
251
251
|
|
|
252
|
-
- `controller.
|
|
252
|
+
- `controller.invoke(command, args, options?)` — iframe의 커맨드 호출, 결과를 Promise로 반환
|
|
253
|
+
- `controller.call(command, args, options?)` — deprecated 호환 alias. `invoke`를 사용하세요.
|
|
253
254
|
- `controller.onNotificationFromIframe(event, handler)` — iframe이 보내는 알림 구독, unsubscribe 함수 반환
|
|
254
255
|
- `controller.ready` — iframe ready 신호 대기용 Promise
|
|
255
256
|
- `controller.terminated` — 종료 사유를 노출하는 Promise (정상 dispose면 `null`)
|
|
@@ -258,11 +259,11 @@ host iframe
|
|
|
258
259
|
|
|
259
260
|
### iframe 진입점
|
|
260
261
|
|
|
261
|
-
| export
|
|
262
|
-
|
|
263
|
-
| `useIframeCallRunner`
|
|
264
|
-
| `createIframeCallRunner`
|
|
265
|
-
| `createParentWindowTransport` | factory | 커스텀 트랜스포트 구성용
|
|
262
|
+
| export | 종류 | 설명 |
|
|
263
|
+
| ----------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
264
|
+
| `useIframeCallRunner` | hook | iframe용 React 훅. `commands`, `iframeHelper`, `isActive`를 반환한다. mount 전 `commands`/`iframeHelper`는 `null`. |
|
|
265
|
+
| `createIframeCallRunner` | factory | 훅 없이 러너를 직접 만들 때 사용 |
|
|
266
|
+
| `createParentWindowTransport` | factory | 커스텀 트랜스포트 구성용 |
|
|
266
267
|
|
|
267
268
|
`iframeHelper`의 주요 멤버:
|
|
268
269
|
|
|
@@ -276,28 +277,71 @@ host iframe
|
|
|
276
277
|
- `_` prefix → 사용자 local-only (dispatch 제외)
|
|
277
278
|
- `$` prefix → 라이브러리 namespace (dispatch 제외, 인식되는 hook은 `$onCommandRun`)
|
|
278
279
|
|
|
279
|
-
`onStatusChange`처럼 prefix가 없으면서 host에서 호출하면 안 되는 메서드는 반드시 `_` prefix를 붙여야 한다. 그렇지 않으면 host가 `controller.
|
|
280
|
+
`onStatusChange`처럼 prefix가 없으면서 host에서 호출하면 안 되는 메서드는 반드시 `_` prefix를 붙여야 한다. 그렇지 않으면 host가 `controller.invoke("onStatusChange", [...])`로 직접 호출할 수 있다.
|
|
280
281
|
|
|
281
|
-
> 알림은 iframe → host
|
|
282
|
+
> 도메인 알림은 iframe → host 단방향이다(라이브러리 내부 lifecycle 메시지 `host:ready-query`는 예외). host → iframe 도메인 알림은 라이브러리 외부에서 `postMessage`로 직접 처리하거나, host에서 커맨드를 호출해 처리한다.
|
|
282
283
|
|
|
283
284
|
### 공통 타입
|
|
284
285
|
|
|
285
|
-
`CommandMap`, `
|
|
286
|
+
`CommandMap`, `IframeCallTransport`, `ReadyPolicy`, `SerializedIframeCallError` 등 공통 타입은 `host`/`iframe` 양쪽에서 export된다. 역할 전용 타입은 한쪽에서만 export된다: `IframeCallController`는 `host`, `IframeCallRunnerHandle`은 `iframe`.
|
|
286
287
|
|
|
287
288
|
## 보안: origin 검증
|
|
288
289
|
|
|
289
|
-
`targetOrigin
|
|
290
|
+
`targetOrigin`은 명시적인 origin으로 지정한다. 와일드카드(`*`)는 사용하지 않는다. `allowedOrigins`를 생략하면 `[targetOrigin]`을 쓴다. opaque origin 모드에서는 둘 다 지정하지 않는다.
|
|
290
291
|
|
|
291
|
-
| 옵션
|
|
292
|
-
|
|
293
|
-
| `targetOrigin`
|
|
292
|
+
| 옵션 | 의미 |
|
|
293
|
+
| ---------------- | ----------------------------------------- |
|
|
294
|
+
| `targetOrigin` | `postMessage` 전송 시 사용할 대상 origin |
|
|
294
295
|
| `allowedOrigins` | 수신 시 허용할 origin 화이트리스트 (배열) |
|
|
295
296
|
|
|
296
|
-
수신 메시지의 `event.origin`이 화이트리스트에 없으면 무시된다.
|
|
297
|
+
수신 메시지의 `event.origin`이 화이트리스트에 없으면 무시된다. host의 기본 transport는 `event.source`가 대상 iframe의 `contentWindow`인지도 함께 검사한다. 단, 일반 모드에서 controller 생성 시점에 iframe이 문서에 붙지 않아 `contentWindow`가 없으면 source 검사를 생략한다. controller는 iframe을 문서에 붙인 뒤 만든다(훅은 ref 연결 후 생성하므로 해당 없음).
|
|
298
|
+
|
|
299
|
+
`targetOrigin`에 `""`, `"*"`, `"null"`을 넘기면 `invalid_origin` 에러를 던진다. origin이 `"null"`인 iframe은 아래 opaque origin 모드를 사용한다.
|
|
300
|
+
|
|
301
|
+
### opaque origin iframe (`sandbox`)
|
|
302
|
+
|
|
303
|
+
`sandbox="allow-scripts"`처럼 `allow-same-origin` 없이 띄운 iframe은 origin이 `"null"`이라 명시적인 `targetOrigin`으로 메시지를 보낼 수 없다. host에서 `opaqueOrigin: true`로 opt-in한다.
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
const { iframeRef, controller, status } = useIframeCallController<AppCommands>({
|
|
307
|
+
opaqueOrigin: true,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return <iframe ref={iframeRef} sandbox="allow-scripts" srcDoc={runnerHtml} />;
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
> **iframe 안에서 신뢰하지 않는 코드가 실행되면 채널 전체를 신뢰하지 않는 상대로 취급한다.** 아래 "보안 근거"를 먼저 읽는다.
|
|
314
|
+
|
|
315
|
+
이 모드의 동작:
|
|
316
|
+
|
|
317
|
+
- 송신: 내부적으로 `targetOrigin="*"`로 보낸다. 대상은 해당 iframe의 `contentWindow`뿐이다.
|
|
318
|
+
- 수신: `event.origin === "null"` **그리고** `event.source === contentWindow`일 때만 받는다.
|
|
319
|
+
- 생성 시 거부(`invalid_origin`):
|
|
320
|
+
- `targetOrigin` 또는 `allowedOrigins`를 함께 지정한 경우. 타입에서도 거부한다.
|
|
321
|
+
- iframe이 문서에 붙지 않아 `contentWindow`가 없는 경우, 또는 커스텀 `transport`에 `expectedSource`가 없는 경우.
|
|
322
|
+
|
|
323
|
+
보안 근거:
|
|
324
|
+
|
|
325
|
+
- **source 비교가 필수인 이유**: origin `"null"`은 다른 사이트의 모든 sandboxed frame이 공유하는 값이다. origin만으로는 대상 iframe과 다른 opaque frame을 구별할 수 없다. 그래서 이 모드는 source를 비교할 수 없는 설정을 생성 시점에 거부한다.
|
|
326
|
+
- **source 검사는 iframe 요소를 인증할 뿐 문서를 인증하지 않는다**: `sandbox="allow-scripts"`는 iframe 문서가 자기 자신을 navigation하는 것(`location.href = ...`)을 막지 않는다. 이동한 문서도 sandbox flag를 이어받아 origin이 `"null"`이고, 같은 요소라 `contentWindow`도 같다. 따라서 그 문서는 host의 origin·source 검사를 모두 통과하고, `"*"`로 보낸 command와 인자를 받으며, 응답과 notify를 보낼 수 있다. (headless Chromium에서 확인)
|
|
327
|
+
- **수용 조건**: 위 위험은 iframe 안에서 신뢰하지 않는 코드가 돌지 않을 때만 수용할 수 있다. 이 경우 `src`/`srcdoc`는 host가 통제하므로 iframe이 모르는 문서로 이동하지 않는다.
|
|
328
|
+
- **신뢰하지 않는 코드를 실행하는 iframe**(사용자 코드 실행 샌드박스 등)에서는 채널 전체를 신뢰하지 않는 상대로 취급한다. command 인자에 비밀이나 권한 토큰을 넣지 않고, 응답과 notify는 host에서 검증한다. 이 경우 iframe 안의 코드는 navigation 없이도 채널을 이미 제어할 수 있다. navigation은 그 제어를 iframe 밖의 문서로 넘기는 경로를 하나 더 만든다.
|
|
329
|
+
|
|
330
|
+
iframe 쪽 runner는 바꿀 필요가 없다. parent(host)는 일반 origin이므로 runner는 지금처럼 `targetOrigin: HOST_ORIGIN`을 명시한다.
|
|
331
|
+
|
|
332
|
+
### iframe 요소 교체
|
|
333
|
+
|
|
334
|
+
controller는 iframe 요소 하나에 묶인다. 같은 요소의 `src` 변경은 `contentWindow` identity를 유지하지만, 요소 자체를 새로 만들면 `contentWindow`가 달라진다. 훅은 요소 교체를 감지하지 않으므로, 요소를 교체할 때는 훅을 소유한 컴포넌트를 `key`로 리마운트한다.
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
<SandboxFrame key={runId} />
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
리마운트하면 이전 controller가 dispose되어 대기 중인 요청이 `terminated`로 reject되고, 새 controller는 이전 window에서 늦게 도착한 메시지를 source 검사로 버린다.
|
|
297
341
|
|
|
298
342
|
## 디버깅
|
|
299
343
|
|
|
300
|
-
훅 옵션에 `debugLog: true
|
|
344
|
+
훅 옵션에 `debugLog: true`(또는 `{ prefix }`)를 주면 송수신 이벤트가 콘솔에 출력된다. 훅 없이 쓸 때는 `controller.debug.subscribe(consoleDebugLogger())`로 같은 출력을 붙인다.
|
|
301
345
|
|
|
302
346
|
```ts
|
|
303
347
|
useIframeCallController({
|
|
@@ -321,6 +365,14 @@ pnpm dev:r19
|
|
|
321
365
|
pnpm dev:r18
|
|
322
366
|
```
|
|
323
367
|
|
|
368
|
+
opaque origin 모드 데모는 `http://localhost:3300/opaque`다. iframe-r19 앱을 `sandbox="allow-scripts"`로 띄운다. Next.js dev server는 origin `"null"` 문서의 `/_next` 리소스 요청을 cross-origin으로 차단하므로 iframe 앱은 production 모드로 실행한다.
|
|
369
|
+
|
|
370
|
+
```sh
|
|
371
|
+
pnpm build
|
|
372
|
+
pnpm --filter iframe-r19 exec next start --port 3301
|
|
373
|
+
pnpm --filter host-r19 dev
|
|
374
|
+
```
|
|
375
|
+
|
|
324
376
|
## 라이선스
|
|
325
377
|
|
|
326
378
|
MIT
|
|
@@ -31,6 +31,7 @@ function isSerializedIframeCallError(value) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// src/core/messages.ts
|
|
34
|
+
var READY_QUERY_EVENT = "host:ready-query";
|
|
34
35
|
function createIframeCallRequest(id, cmd, args) {
|
|
35
36
|
return {
|
|
36
37
|
protocol: "iframecall",
|
|
@@ -111,10 +112,12 @@ function isRecord(value) {
|
|
|
111
112
|
|
|
112
113
|
// src/core/transport.ts
|
|
113
114
|
function createIframeWindowTransport(iframe) {
|
|
115
|
+
var _a;
|
|
114
116
|
return {
|
|
115
|
-
expectedSource: iframe.contentWindow
|
|
117
|
+
expectedSource: (_a = iframe.contentWindow) != null ? _a : void 0,
|
|
116
118
|
post(message, targetOrigin, transfer) {
|
|
117
|
-
|
|
119
|
+
var _a2;
|
|
120
|
+
(_a2 = iframe.contentWindow) == null ? void 0 : _a2.postMessage(
|
|
118
121
|
message,
|
|
119
122
|
targetOrigin,
|
|
120
123
|
transfer
|
|
@@ -161,15 +164,33 @@ function createParentWindowTransport() {
|
|
|
161
164
|
};
|
|
162
165
|
}
|
|
163
166
|
|
|
167
|
+
// src/core/inboundValidation.ts
|
|
168
|
+
function validateInbound(event, policy) {
|
|
169
|
+
if (!policy.allowedOrigins.has(event.origin)) {
|
|
170
|
+
return { accepted: false, reason: "origin" };
|
|
171
|
+
}
|
|
172
|
+
if (policy.expectedSource === void 0) {
|
|
173
|
+
if (policy.requireSource === true) {
|
|
174
|
+
return { accepted: false, reason: "source" };
|
|
175
|
+
}
|
|
176
|
+
} else if (event.source !== policy.expectedSource) {
|
|
177
|
+
return { accepted: false, reason: "source" };
|
|
178
|
+
}
|
|
179
|
+
const message = parseIframeCallMessage(event.data);
|
|
180
|
+
return message === null ? { accepted: false, reason: "message" } : { accepted: true, message };
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
export {
|
|
165
184
|
createIframeCallError,
|
|
166
185
|
serializeIframeCallError,
|
|
167
186
|
isSerializedIframeCallError,
|
|
187
|
+
READY_QUERY_EVENT,
|
|
168
188
|
createIframeCallRequest,
|
|
169
189
|
createIframeCallSuccessResponse,
|
|
170
190
|
createIframeCallErrorResponse,
|
|
171
191
|
createIframeCallNotify,
|
|
172
192
|
parseIframeCallMessage,
|
|
173
193
|
createIframeWindowTransport,
|
|
174
|
-
createParentWindowTransport
|
|
194
|
+
createParentWindowTransport,
|
|
195
|
+
validateInbound
|
|
175
196
|
};
|
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 {
|
|
1
|
+
import { H as HostDebugEvent, C as CommandMap, I as IframeCallControllerOptions, a as IframeCallController, b as IframeCallControllerOriginOptions, c as IframeCallControllerBaseOptions, S as SerializedIframeCallError } from './messages-Bk4RCDOd.js';
|
|
2
|
+
export { d as CommandArgs, e as CommandHandler, f as CommandResult, g as CommandRunner, h as IframeCallCallOptions, i as IframeCallLogger, j as IframeCallNotify, k as IframeCallRequest, l as IframeCallResponse, m as IframeCallTransferable, n as IframeCallTransport, o as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, p as ReservedNotificationName, q as createIframeCallError, r as createIframeCallErrorResponse, s as createIframeCallNotify, t as createIframeCallRequest, u as createIframeCallSuccessResponse, v as createIframeWindowTransport, w as isSerializedIframeCallError, x as parseIframeCallMessage, y as serializeIframeCallError } from './messages-Bk4RCDOd.js';
|
|
3
3
|
|
|
4
4
|
/** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:host]가 기본값이다. */
|
|
5
5
|
type ConsoleDebugLoggerOptions = {
|
|
@@ -29,10 +29,9 @@ type UseIframeCallControllerDebugLog = boolean | {
|
|
|
29
29
|
* useIframeCallController에 전달하는 옵션.
|
|
30
30
|
* iframe element는 callback ref로 받으므로 controller 생성 옵션에서 제외한다.
|
|
31
31
|
*/
|
|
32
|
-
type UseIframeCallControllerOptions<TCommands extends CommandMap<TCommands>> = {
|
|
33
|
-
readonly targetOrigin: string;
|
|
32
|
+
type UseIframeCallControllerOptions<TCommands extends CommandMap<TCommands>> = IframeCallControllerOriginOptions & {
|
|
34
33
|
readonly debugLog?: UseIframeCallControllerDebugLog;
|
|
35
|
-
} & Pick<
|
|
34
|
+
} & Pick<IframeCallControllerBaseOptions<TCommands>, "defaultTimeoutMs" | "generateId" | "logger" | "readyPolicy" | "readyQueueLimit" | "readyTimeoutMs" | "transport">;
|
|
36
35
|
/** useIframeCallController가 반환하는 handle. mount 전에는 controller가 null이다. */
|
|
37
36
|
type UseIframeCallControllerResult<TCommands extends CommandMap<TCommands>, TNotificationsFromIframe = Record<string, unknown>> = {
|
|
38
37
|
readonly iframeRef: (node: HTMLIFrameElement | null) => void;
|
|
@@ -49,4 +48,4 @@ type UseIframeCallControllerResult<TCommands extends CommandMap<TCommands>, TNot
|
|
|
49
48
|
*/
|
|
50
49
|
declare function useIframeCallController<TCommands extends CommandMap<TCommands>, TNotificationsFromIframe = Record<string, unknown>>(options: UseIframeCallControllerOptions<TCommands>): UseIframeCallControllerResult<TCommands, TNotificationsFromIframe>;
|
|
51
50
|
|
|
52
|
-
export { CommandMap, HostDebugEvent, IframeCallController, IframeCallControllerOptions, type IframeCallControllerStatus, SerializedIframeCallError, type UseIframeCallControllerDebugLog, type UseIframeCallControllerOptions, type UseIframeCallControllerResult, consoleDebugLogger, createIframeCallController, useIframeCallController };
|
|
51
|
+
export { CommandMap, HostDebugEvent, IframeCallController, IframeCallControllerOptions, IframeCallControllerOriginOptions, type IframeCallControllerStatus, SerializedIframeCallError, type UseIframeCallControllerDebugLog, type UseIframeCallControllerOptions, type UseIframeCallControllerResult, consoleDebugLogger, createIframeCallController, useIframeCallController };
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
READY_QUERY_EVENT,
|
|
2
3
|
createIframeCallError,
|
|
3
4
|
createIframeCallErrorResponse,
|
|
4
5
|
createIframeCallNotify,
|
|
@@ -7,12 +8,14 @@ import {
|
|
|
7
8
|
createIframeWindowTransport,
|
|
8
9
|
isSerializedIframeCallError,
|
|
9
10
|
parseIframeCallMessage,
|
|
10
|
-
serializeIframeCallError
|
|
11
|
-
|
|
11
|
+
serializeIframeCallError,
|
|
12
|
+
validateInbound
|
|
13
|
+
} from "./chunk-UNM5VKFB.js";
|
|
12
14
|
|
|
13
15
|
// src/host/consoleDebugLogger.ts
|
|
14
16
|
function consoleDebugLogger(options = {}) {
|
|
15
|
-
|
|
17
|
+
var _a;
|
|
18
|
+
const prefix = (_a = options.prefix) != null ? _a : "[iframecall:host]";
|
|
16
19
|
const head = prefix.length > 0 ? `${prefix} ` : "";
|
|
17
20
|
return (event) => {
|
|
18
21
|
switch (event.type) {
|
|
@@ -95,7 +98,7 @@ function createControllerLifecycle(options) {
|
|
|
95
98
|
return;
|
|
96
99
|
}
|
|
97
100
|
cleanupDone = true;
|
|
98
|
-
onCleanup
|
|
101
|
+
onCleanup == null ? void 0 : onCleanup();
|
|
99
102
|
}
|
|
100
103
|
return {
|
|
101
104
|
ready,
|
|
@@ -117,7 +120,8 @@ function createNotifyHandlerRegistry() {
|
|
|
117
120
|
const handlers = /* @__PURE__ */ new Map();
|
|
118
121
|
return {
|
|
119
122
|
register(event, handler) {
|
|
120
|
-
|
|
123
|
+
var _a;
|
|
124
|
+
const set = (_a = handlers.get(event)) != null ? _a : /* @__PURE__ */ new Set();
|
|
121
125
|
set.add(handler);
|
|
122
126
|
handlers.set(event, set);
|
|
123
127
|
return () => {
|
|
@@ -142,144 +146,173 @@ function createNotifyHandlerRegistry() {
|
|
|
142
146
|
};
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
// src/host/
|
|
146
|
-
function
|
|
149
|
+
// src/host/invocationLedger.ts
|
|
150
|
+
function createInvocationLedger(options) {
|
|
151
|
+
const queued = /* @__PURE__ */ new Map();
|
|
147
152
|
const pending = /* @__PURE__ */ new Map();
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
let ready = false;
|
|
154
|
+
function clearDeadline(invocation) {
|
|
155
|
+
if (invocation.timeoutId !== null) {
|
|
156
|
+
clearTimeout(invocation.timeoutId);
|
|
151
157
|
}
|
|
152
158
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
159
|
+
function rejectInvocation(invocation, error) {
|
|
160
|
+
clearDeadline(invocation);
|
|
161
|
+
invocation.reject(error);
|
|
162
|
+
}
|
|
163
|
+
function expire(id, command, timeoutMs) {
|
|
164
|
+
var _a;
|
|
165
|
+
const invocation = (_a = queued.get(id)) != null ? _a : pending.get(id);
|
|
166
|
+
if (invocation === void 0) return;
|
|
167
|
+
queued.delete(id);
|
|
168
|
+
pending.delete(id);
|
|
169
|
+
rejectInvocation(
|
|
170
|
+
invocation,
|
|
171
|
+
createIframeCallError("timeout", "Command timed out.", {
|
|
172
|
+
command,
|
|
173
|
+
details: { timeoutMs }
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
function post(id, invocation) {
|
|
178
|
+
options.onCommandSent(invocation.command, invocation.args);
|
|
179
|
+
pending.set(id, invocation);
|
|
180
|
+
try {
|
|
181
|
+
options.transport.post(
|
|
182
|
+
createIframeCallRequest(id, invocation.command, invocation.args),
|
|
183
|
+
options.targetOrigin,
|
|
184
|
+
invocation.transfer
|
|
185
|
+
);
|
|
186
|
+
} catch (error) {
|
|
165
187
|
pending.delete(id);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
post(id, cmd, args, transfer) {
|
|
174
|
-
try {
|
|
175
|
-
transport.post(
|
|
176
|
-
createIframeCallRequest(id, cmd, args),
|
|
177
|
-
targetOrigin,
|
|
178
|
-
transfer
|
|
179
|
-
);
|
|
180
|
-
} catch (error) {
|
|
181
|
-
const call = pending.get(id);
|
|
182
|
-
pending.delete(id);
|
|
183
|
-
if (call !== void 0) {
|
|
184
|
-
clearCallTimeout(call);
|
|
185
|
-
call.reject(
|
|
186
|
-
createIframeCallError(
|
|
187
|
-
"invalid_args",
|
|
188
|
-
"Failed to post command request.",
|
|
189
|
-
{
|
|
190
|
-
command: cmd,
|
|
191
|
-
details: error
|
|
192
|
-
}
|
|
193
|
-
)
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
},
|
|
198
|
-
rejectAll(buildError) {
|
|
199
|
-
for (const [, call] of pending) {
|
|
200
|
-
clearCallTimeout(call);
|
|
201
|
-
call.reject(buildError(call.command));
|
|
202
|
-
}
|
|
203
|
-
pending.clear();
|
|
204
|
-
},
|
|
205
|
-
getCommand(id) {
|
|
206
|
-
return pending.get(id)?.command;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// src/host/readyQueue.ts
|
|
212
|
-
function createReadyQueue() {
|
|
213
|
-
const queued = /* @__PURE__ */ new Map();
|
|
214
|
-
function clearCallTimeout(call) {
|
|
215
|
-
if (call.timeoutId !== null) {
|
|
216
|
-
clearTimeout(call.timeoutId);
|
|
188
|
+
rejectInvocation(
|
|
189
|
+
invocation,
|
|
190
|
+
createIframeCallError("invalid_args", "Failed to post command request.", {
|
|
191
|
+
command: invocation.command,
|
|
192
|
+
details: error
|
|
193
|
+
})
|
|
194
|
+
);
|
|
217
195
|
}
|
|
218
196
|
}
|
|
219
197
|
return {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
198
|
+
invoke(generateId, command, args, callOptions) {
|
|
199
|
+
if (!ready && options.readyPolicy === "reject") {
|
|
200
|
+
return Promise.reject(
|
|
201
|
+
createIframeCallError("not_ready", "Iframe is not ready.", {
|
|
202
|
+
command
|
|
203
|
+
})
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
return new Promise((resolve, reject) => {
|
|
207
|
+
const id = generateId();
|
|
208
|
+
const timeoutMs = callOptions.timeoutMs;
|
|
209
|
+
const timeoutId = timeoutMs === 0 || timeoutMs === Number.POSITIVE_INFINITY ? null : setTimeout(() => expire(id, command, timeoutMs), timeoutMs);
|
|
210
|
+
const invocation = {
|
|
211
|
+
command,
|
|
212
|
+
args,
|
|
213
|
+
transfer: callOptions.transfer,
|
|
214
|
+
timeoutId,
|
|
215
|
+
resolve,
|
|
216
|
+
reject
|
|
217
|
+
};
|
|
218
|
+
if (!ready) {
|
|
219
|
+
if (queued.size >= options.readyQueueLimit) {
|
|
220
|
+
clearDeadline(invocation);
|
|
221
|
+
reject(
|
|
222
|
+
createIframeCallError("queue_overflow", "Ready queue overflow.", {
|
|
223
|
+
command,
|
|
224
|
+
details: { readyQueueLimit: options.readyQueueLimit }
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
queued.set(id, invocation);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
post(id, invocation);
|
|
233
|
+
});
|
|
228
234
|
},
|
|
229
|
-
|
|
230
|
-
|
|
235
|
+
acceptReady() {
|
|
236
|
+
if (ready) return;
|
|
237
|
+
ready = true;
|
|
238
|
+
for (const [id, invocation] of queued) {
|
|
231
239
|
queued.delete(id);
|
|
232
|
-
|
|
240
|
+
post(id, invocation);
|
|
233
241
|
}
|
|
234
242
|
},
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
243
|
+
settle(response) {
|
|
244
|
+
const invocation = pending.get(response.id);
|
|
245
|
+
if (invocation === void 0) return void 0;
|
|
246
|
+
pending.delete(response.id);
|
|
247
|
+
clearDeadline(invocation);
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
invocation.resolve(response.value);
|
|
250
|
+
} else {
|
|
251
|
+
invocation.reject(response.error);
|
|
252
|
+
}
|
|
253
|
+
return invocation.command;
|
|
254
|
+
},
|
|
255
|
+
terminate(buildError) {
|
|
256
|
+
for (const [, invocation] of queued) {
|
|
257
|
+
rejectInvocation(invocation, buildError(invocation.command));
|
|
239
258
|
}
|
|
240
259
|
queued.clear();
|
|
260
|
+
for (const [, invocation] of pending) {
|
|
261
|
+
rejectInvocation(invocation, buildError(invocation.command));
|
|
262
|
+
}
|
|
263
|
+
pending.clear();
|
|
241
264
|
}
|
|
242
265
|
};
|
|
243
266
|
}
|
|
244
267
|
|
|
245
268
|
// src/host/controller.ts
|
|
246
269
|
function createIframeCallController(options) {
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
const
|
|
256
|
-
const
|
|
270
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
271
|
+
const transport = (_a = options.transport) != null ? _a : createIframeWindowTransport(options.iframe);
|
|
272
|
+
const { targetOrigin, allowedOrigins, requireSource } = resolveOriginPolicy(
|
|
273
|
+
options,
|
|
274
|
+
transport
|
|
275
|
+
);
|
|
276
|
+
const generateId = (_b = options.generateId) != null ? _b : createDefaultRequestId;
|
|
277
|
+
const defaultTimeoutMs = (_c = options.defaultTimeoutMs) != null ? _c : 3e4;
|
|
278
|
+
const readyTimeoutMs = (_d = options.readyTimeoutMs) != null ? _d : defaultTimeoutMs;
|
|
279
|
+
const readyPolicy = (_e = options.readyPolicy) != null ? _e : "queue";
|
|
280
|
+
const readyQueueLimit = (_f = options.readyQueueLimit) != null ? _f : Number.POSITIVE_INFINITY;
|
|
257
281
|
const notifyRegistry = createNotifyHandlerRegistry();
|
|
258
282
|
const debugSubscribers = /* @__PURE__ */ new Set();
|
|
259
283
|
function emitDebug(event) {
|
|
284
|
+
var _a2;
|
|
260
285
|
for (const handler of debugSubscribers) {
|
|
261
286
|
try {
|
|
262
287
|
handler(event);
|
|
263
288
|
} catch (error) {
|
|
264
|
-
options.logger
|
|
289
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall debug subscriber threw.", error);
|
|
265
290
|
}
|
|
266
291
|
}
|
|
267
292
|
}
|
|
293
|
+
const ledger = createInvocationLedger({
|
|
294
|
+
transport,
|
|
295
|
+
targetOrigin,
|
|
296
|
+
readyPolicy,
|
|
297
|
+
readyQueueLimit,
|
|
298
|
+
onCommandSent(command, args) {
|
|
299
|
+
emitDebug({ type: "commandSentToIframe", command, args });
|
|
300
|
+
}
|
|
301
|
+
});
|
|
268
302
|
const lifecycle = createControllerLifecycle({
|
|
269
303
|
readyTimeoutMs,
|
|
270
304
|
onTerminate(error) {
|
|
271
305
|
const buildLifecycleError = (command) => createCallLifecycleError(error, command);
|
|
272
|
-
|
|
273
|
-
queue.rejectAll(buildLifecycleError);
|
|
306
|
+
ledger.terminate(buildLifecycleError);
|
|
274
307
|
}
|
|
275
308
|
});
|
|
276
309
|
const unsubscribeTransport = transport.subscribe(
|
|
277
310
|
createTransportRouter({
|
|
278
311
|
lifecycle,
|
|
279
312
|
allowedOrigins,
|
|
313
|
+
requireSource,
|
|
280
314
|
transport,
|
|
281
|
-
|
|
282
|
-
queue,
|
|
315
|
+
ledger,
|
|
283
316
|
notifyRegistry,
|
|
284
317
|
logger: options.logger,
|
|
285
318
|
emitDebug
|
|
@@ -289,68 +322,32 @@ function createIframeCallController(options) {
|
|
|
289
322
|
unsubscribeTransport();
|
|
290
323
|
notifyRegistry.clear();
|
|
291
324
|
});
|
|
325
|
+
try {
|
|
326
|
+
transport.post(
|
|
327
|
+
createIframeCallNotify(READY_QUERY_EVENT, null),
|
|
328
|
+
targetOrigin
|
|
329
|
+
);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
(_g = options.logger) == null ? void 0 : _g.warn("iframecall postMessage failed.", error);
|
|
332
|
+
}
|
|
333
|
+
const invoke = (cmd, args, callOptions) => {
|
|
334
|
+
var _a2;
|
|
335
|
+
const terminatedError = lifecycle.getTerminatedError();
|
|
336
|
+
if (terminatedError !== null) {
|
|
337
|
+
return Promise.reject(terminatedError);
|
|
338
|
+
}
|
|
339
|
+
const timeoutMs = (_a2 = callOptions == null ? void 0 : callOptions.timeoutMs) != null ? _a2 : defaultTimeoutMs;
|
|
340
|
+
return ledger.invoke(generateId, cmd, args, {
|
|
341
|
+
timeoutMs,
|
|
342
|
+
transfer: callOptions == null ? void 0 : callOptions.transfer
|
|
343
|
+
});
|
|
344
|
+
};
|
|
292
345
|
const controller = {
|
|
293
346
|
ready: lifecycle.ready,
|
|
294
347
|
terminated: lifecycle.terminated,
|
|
348
|
+
invoke,
|
|
295
349
|
call(cmd, args, callOptions) {
|
|
296
|
-
|
|
297
|
-
if (terminatedError !== null) {
|
|
298
|
-
return Promise.reject(terminatedError);
|
|
299
|
-
}
|
|
300
|
-
if (!lifecycle.isReady() && readyPolicy === "reject") {
|
|
301
|
-
return Promise.reject(
|
|
302
|
-
createIframeCallError("not_ready", "Iframe is not ready.", {
|
|
303
|
-
command: cmd
|
|
304
|
-
})
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
const id = generateId();
|
|
308
|
-
const timeoutMs = callOptions?.timeoutMs ?? defaultTimeoutMs;
|
|
309
|
-
return new Promise((resolve, reject) => {
|
|
310
|
-
const timeoutId = timeoutMs === 0 || timeoutMs === Number.POSITIVE_INFINITY ? null : setTimeout(() => {
|
|
311
|
-
pending.delete(id);
|
|
312
|
-
queue.delete(id);
|
|
313
|
-
reject(
|
|
314
|
-
createIframeCallError("timeout", "Command timed out.", {
|
|
315
|
-
command: cmd,
|
|
316
|
-
details: { timeoutMs }
|
|
317
|
-
})
|
|
318
|
-
);
|
|
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
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
emitDebug({ type: "commandSentToIframe", command: cmd, args });
|
|
351
|
-
pending.add(id, call);
|
|
352
|
-
pending.post(id, cmd, args, callOptions?.transfer);
|
|
353
|
-
});
|
|
350
|
+
return invoke(cmd, args, callOptions);
|
|
354
351
|
},
|
|
355
352
|
onNotificationFromIframe(event, handler) {
|
|
356
353
|
return notifyRegistry.register(
|
|
@@ -367,6 +364,7 @@ function createIframeCallController(options) {
|
|
|
367
364
|
}
|
|
368
365
|
},
|
|
369
366
|
async dispose(reason = "host_requested") {
|
|
367
|
+
var _a2;
|
|
370
368
|
if (lifecycle.isTerminated()) {
|
|
371
369
|
lifecycle.cleanup();
|
|
372
370
|
return;
|
|
@@ -384,7 +382,7 @@ function createIframeCallController(options) {
|
|
|
384
382
|
targetOrigin
|
|
385
383
|
);
|
|
386
384
|
} catch (error) {
|
|
387
|
-
options.logger
|
|
385
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall dispose message failed.", error);
|
|
388
386
|
} finally {
|
|
389
387
|
lifecycle.terminate(lifecycleError);
|
|
390
388
|
lifecycle.cleanup();
|
|
@@ -393,28 +391,37 @@ function createIframeCallController(options) {
|
|
|
393
391
|
};
|
|
394
392
|
return controller;
|
|
395
393
|
}
|
|
394
|
+
function createDefaultRequestId() {
|
|
395
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
396
|
+
let id = "";
|
|
397
|
+
for (const byte of bytes) {
|
|
398
|
+
id += byte.toString(16).padStart(2, "0");
|
|
399
|
+
}
|
|
400
|
+
return id;
|
|
401
|
+
}
|
|
396
402
|
function createTransportRouter(deps) {
|
|
397
403
|
const {
|
|
398
404
|
lifecycle,
|
|
399
405
|
allowedOrigins,
|
|
406
|
+
requireSource,
|
|
400
407
|
transport,
|
|
401
|
-
|
|
402
|
-
queue,
|
|
408
|
+
ledger,
|
|
403
409
|
notifyRegistry,
|
|
404
410
|
logger,
|
|
405
411
|
emitDebug
|
|
406
412
|
} = deps;
|
|
407
413
|
return (event) => {
|
|
408
414
|
if (lifecycle.isTerminated()) return;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
+
const inbound = validateInbound(event, {
|
|
416
|
+
allowedOrigins,
|
|
417
|
+
expectedSource: transport.expectedSource,
|
|
418
|
+
requireSource
|
|
419
|
+
});
|
|
420
|
+
if (!inbound.accepted) return;
|
|
421
|
+
const parsed = inbound.message;
|
|
422
|
+
if ((parsed == null ? void 0 : parsed.type) === "response") {
|
|
415
423
|
const responseMessage = parsed.message;
|
|
416
|
-
const command =
|
|
417
|
-
pending.settle(responseMessage.id, responseMessage);
|
|
424
|
+
const command = ledger.settle(responseMessage);
|
|
418
425
|
if (command !== void 0) {
|
|
419
426
|
if (responseMessage.ok) {
|
|
420
427
|
emitDebug({
|
|
@@ -432,10 +439,10 @@ function createTransportRouter(deps) {
|
|
|
432
439
|
}
|
|
433
440
|
return;
|
|
434
441
|
}
|
|
435
|
-
if (parsed
|
|
442
|
+
if ((parsed == null ? void 0 : parsed.type) !== "notify") return;
|
|
436
443
|
const { event: notifyEvent, payload } = parsed.message;
|
|
437
444
|
if (notifyEvent === "ready") {
|
|
438
|
-
handleReadyNotify(payload, lifecycle,
|
|
445
|
+
handleReadyNotify(payload, lifecycle, ledger, logger, emitDebug);
|
|
439
446
|
return;
|
|
440
447
|
}
|
|
441
448
|
if (notifyEvent === "terminated") {
|
|
@@ -445,7 +452,7 @@ function createTransportRouter(deps) {
|
|
|
445
452
|
emitDebug({
|
|
446
453
|
type: "terminatedReceived",
|
|
447
454
|
reason,
|
|
448
|
-
error: cause
|
|
455
|
+
error: cause != null ? cause : null
|
|
449
456
|
});
|
|
450
457
|
lifecycle.terminate(
|
|
451
458
|
createIframeCallError("terminated", "Iframe terminated.", {
|
|
@@ -463,9 +470,11 @@ function createTransportRouter(deps) {
|
|
|
463
470
|
});
|
|
464
471
|
};
|
|
465
472
|
}
|
|
466
|
-
function handleReadyNotify(payload, lifecycle,
|
|
473
|
+
function handleReadyNotify(payload, lifecycle, ledger, logger, emitDebug) {
|
|
467
474
|
if (lifecycle.isReady()) {
|
|
468
|
-
|
|
475
|
+
if (!(isRecord(payload) && payload.requested === true)) {
|
|
476
|
+
logger == null ? void 0 : logger.warn("iframecall duplicate ready ignored.", payload);
|
|
477
|
+
}
|
|
469
478
|
return;
|
|
470
479
|
}
|
|
471
480
|
if (!isSupportedReadyPayload(payload)) {
|
|
@@ -480,15 +489,7 @@ function handleReadyNotify(payload, lifecycle, queue, pending, logger, emitDebug
|
|
|
480
489
|
}
|
|
481
490
|
emitDebug({ type: "readyReceived", payload });
|
|
482
491
|
lifecycle.markReady();
|
|
483
|
-
|
|
484
|
-
emitDebug({
|
|
485
|
-
type: "commandSentToIframe",
|
|
486
|
-
command: call.command,
|
|
487
|
-
args: call.args
|
|
488
|
-
});
|
|
489
|
-
pending.add(id, call);
|
|
490
|
-
pending.post(id, call.command, call.args, call.transfer);
|
|
491
|
-
});
|
|
492
|
+
ledger.acceptReady();
|
|
492
493
|
}
|
|
493
494
|
function createCallLifecycleError(error, command) {
|
|
494
495
|
if (error.code !== "timeout" || error.command !== void 0) {
|
|
@@ -508,6 +509,34 @@ function getTerminatedCause(payload) {
|
|
|
508
509
|
function isSupportedReadyPayload(payload) {
|
|
509
510
|
return isRecord(payload) && payload.protocolVersion === 1;
|
|
510
511
|
}
|
|
512
|
+
function resolveOriginPolicy(options, transport) {
|
|
513
|
+
var _a;
|
|
514
|
+
if (options.opaqueOrigin !== true) {
|
|
515
|
+
const targetOrigin = requireTargetOrigin(options.targetOrigin);
|
|
516
|
+
return {
|
|
517
|
+
targetOrigin,
|
|
518
|
+
allowedOrigins: new Set((_a = options.allowedOrigins) != null ? _a : [targetOrigin]),
|
|
519
|
+
requireSource: false
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
if (options.targetOrigin !== void 0 || options.allowedOrigins !== void 0) {
|
|
523
|
+
throw createIframeCallError(
|
|
524
|
+
"invalid_origin",
|
|
525
|
+
"opaqueOrigin cannot be combined with targetOrigin or allowedOrigins."
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
if (transport.expectedSource === void 0) {
|
|
529
|
+
throw createIframeCallError(
|
|
530
|
+
"invalid_origin",
|
|
531
|
+
"opaqueOrigin requires transport.expectedSource."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
targetOrigin: "*",
|
|
536
|
+
allowedOrigins: /* @__PURE__ */ new Set(["null"]),
|
|
537
|
+
requireSource: true
|
|
538
|
+
};
|
|
539
|
+
}
|
|
511
540
|
function requireTargetOrigin(targetOrigin) {
|
|
512
541
|
if (targetOrigin.length === 0 || targetOrigin === "*" || targetOrigin === "null") {
|
|
513
542
|
throw createIframeCallError(
|
|
@@ -532,19 +561,20 @@ function useIframeCallController(options) {
|
|
|
532
561
|
const [status, setStatus] = useState("pending");
|
|
533
562
|
const [terminationError, setTerminationError] = useState(null);
|
|
534
563
|
const [readyError, setReadyError] = useState(null);
|
|
564
|
+
const controllerRef = useRef(null);
|
|
535
565
|
const iframeRef = useCallback((node) => {
|
|
536
566
|
iframeElRef.current = node;
|
|
537
567
|
setIframeAttached(node !== null);
|
|
538
568
|
}, []);
|
|
539
569
|
useEffect(() => {
|
|
540
570
|
if (!iframeAttached) return;
|
|
571
|
+
if (controllerRef.current !== null) return;
|
|
541
572
|
const iframeEl = iframeElRef.current;
|
|
542
573
|
if (iframeEl === null) return;
|
|
543
574
|
const opts = optionsRef.current;
|
|
544
575
|
const next = createIframeCallController({
|
|
545
576
|
iframe: iframeEl,
|
|
546
|
-
|
|
547
|
-
allowedOrigins: opts.allowedOrigins,
|
|
577
|
+
...pickOriginOptions(opts),
|
|
548
578
|
defaultTimeoutMs: opts.defaultTimeoutMs,
|
|
549
579
|
generateId: opts.generateId,
|
|
550
580
|
logger: opts.logger,
|
|
@@ -553,13 +583,15 @@ function useIframeCallController(options) {
|
|
|
553
583
|
readyTimeoutMs: opts.readyTimeoutMs,
|
|
554
584
|
transport: opts.transport
|
|
555
585
|
});
|
|
586
|
+
const controller2 = next;
|
|
587
|
+
controllerRef.current = controller2;
|
|
556
588
|
let cancelled = false;
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
589
|
+
if (controllerRef.current === controller2) {
|
|
590
|
+
setController(controller2);
|
|
591
|
+
setStatus("pending");
|
|
592
|
+
setTerminationError(null);
|
|
593
|
+
setReadyError(null);
|
|
594
|
+
}
|
|
563
595
|
next.ready.then(
|
|
564
596
|
() => {
|
|
565
597
|
if (cancelled) return;
|
|
@@ -591,6 +623,7 @@ function useIframeCallController(options) {
|
|
|
591
623
|
unsubscribeDebug();
|
|
592
624
|
}
|
|
593
625
|
void next.dispose("host-unmount");
|
|
626
|
+
if (controllerRef.current === controller2) controllerRef.current = null;
|
|
594
627
|
setController(null);
|
|
595
628
|
};
|
|
596
629
|
}, [iframeAttached]);
|
|
@@ -602,6 +635,13 @@ function useIframeCallController(options) {
|
|
|
602
635
|
readyError
|
|
603
636
|
};
|
|
604
637
|
}
|
|
638
|
+
function pickOriginOptions(opts) {
|
|
639
|
+
return {
|
|
640
|
+
opaqueOrigin: opts.opaqueOrigin,
|
|
641
|
+
targetOrigin: opts.targetOrigin,
|
|
642
|
+
allowedOrigins: opts.allowedOrigins
|
|
643
|
+
};
|
|
644
|
+
}
|
|
605
645
|
export {
|
|
606
646
|
consoleDebugLogger,
|
|
607
647
|
createIframeCallController,
|
package/dist/iframe.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { z as IframeDebugEvent, C as CommandMap, A as IframeCallRunnerOptions, B as IframeCallRunnerHandle, D as CommandsConstructor, E as IframeCallRunnerClassOptions, F as IframeHelper } from './messages-Bk4RCDOd.js';
|
|
2
|
+
export { d as CommandArgs, e as CommandHandler, f as CommandResult, g as CommandRunner, G as DomainNotificationKey, h as IframeCallCallOptions, i as IframeCallLogger, j as IframeCallNotify, k as IframeCallRequest, l as IframeCallResponse, m as IframeCallTransferable, n as IframeCallTransport, o as IframeCallTransportEvent, N as NotifyHandler, P as ParsedIframeCallMessage, R as ReadyPolicy, p as ReservedNotificationName, S as SerializedIframeCallError, q as createIframeCallError, r as createIframeCallErrorResponse, s as createIframeCallNotify, t as createIframeCallRequest, u as createIframeCallSuccessResponse, J as createParentWindowTransport, w as isSerializedIframeCallError, x as parseIframeCallMessage, y as serializeIframeCallError } from './messages-Bk4RCDOd.js';
|
|
3
3
|
|
|
4
4
|
/** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:iframe]가 기본값이다. */
|
|
5
5
|
type ConsoleDebugLoggerOptions = {
|
package/dist/iframe.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
READY_QUERY_EVENT,
|
|
2
3
|
createIframeCallError,
|
|
3
4
|
createIframeCallErrorResponse,
|
|
4
5
|
createIframeCallNotify,
|
|
@@ -7,12 +8,14 @@ import {
|
|
|
7
8
|
createParentWindowTransport,
|
|
8
9
|
isSerializedIframeCallError,
|
|
9
10
|
parseIframeCallMessage,
|
|
10
|
-
serializeIframeCallError
|
|
11
|
-
|
|
11
|
+
serializeIframeCallError,
|
|
12
|
+
validateInbound
|
|
13
|
+
} from "./chunk-UNM5VKFB.js";
|
|
12
14
|
|
|
13
15
|
// src/iframe/consoleDebugLogger.ts
|
|
14
16
|
function consoleDebugLogger(options = {}) {
|
|
15
|
-
|
|
17
|
+
var _a;
|
|
18
|
+
const prefix = (_a = options.prefix) != null ? _a : "[iframecall:iframe]";
|
|
16
19
|
const head = prefix.length > 0 ? `${prefix} ` : "";
|
|
17
20
|
return (event) => {
|
|
18
21
|
switch (event.type) {
|
|
@@ -38,18 +41,21 @@ function consoleDebugLogger(options = {}) {
|
|
|
38
41
|
// src/iframe/runner.ts
|
|
39
42
|
var RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set(["constructor", "host:dispose"]);
|
|
40
43
|
function createIframeCallRunner(options) {
|
|
44
|
+
var _a, _b;
|
|
41
45
|
const targetOrigin = requireTargetOrigin(options.targetOrigin);
|
|
42
|
-
const allowedOrigins = new Set(options.allowedOrigins
|
|
43
|
-
const transport = options.transport
|
|
46
|
+
const allowedOrigins = new Set((_a = options.allowedOrigins) != null ? _a : [targetOrigin]);
|
|
47
|
+
const transport = (_b = options.transport) != null ? _b : createParentWindowTransport();
|
|
44
48
|
let disposing = false;
|
|
45
49
|
let disposed = false;
|
|
50
|
+
let lifecycleReadySent = false;
|
|
46
51
|
const debugSubscribers = /* @__PURE__ */ new Set();
|
|
47
52
|
function emitDebug(event) {
|
|
53
|
+
var _a2;
|
|
48
54
|
for (const handler of debugSubscribers) {
|
|
49
55
|
try {
|
|
50
56
|
handler(event);
|
|
51
57
|
} catch (error) {
|
|
52
|
-
options.logger
|
|
58
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall debug subscriber threw.", error);
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -61,6 +67,7 @@ function createIframeCallRunner(options) {
|
|
|
61
67
|
},
|
|
62
68
|
sendLifecycleReady() {
|
|
63
69
|
if (disposing || disposed) return;
|
|
70
|
+
lifecycleReadySent = true;
|
|
64
71
|
safePost(createIframeCallNotify("ready", { protocolVersion: 1 }));
|
|
65
72
|
},
|
|
66
73
|
debug: {
|
|
@@ -76,14 +83,24 @@ function createIframeCallRunner(options) {
|
|
|
76
83
|
const { commands, dispatch, commandRunHook } = resolveCommandSource(options, iframeHelper);
|
|
77
84
|
const unsubscribeTransport = transport.subscribe((event) => {
|
|
78
85
|
if (disposing || disposed) return;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
const inbound = validateInbound(event, {
|
|
87
|
+
allowedOrigins,
|
|
88
|
+
expectedSource: transport.expectedSource
|
|
89
|
+
});
|
|
90
|
+
if (!inbound.accepted) return;
|
|
91
|
+
const parsed = inbound.message;
|
|
92
|
+
if ((parsed == null ? void 0 : parsed.type) === "notify") {
|
|
93
|
+
if (parsed.message.event === READY_QUERY_EVENT) {
|
|
94
|
+
if (lifecycleReadySent) {
|
|
95
|
+
safePost(
|
|
96
|
+
createIframeCallNotify("ready", {
|
|
97
|
+
protocolVersion: 1,
|
|
98
|
+
requested: true
|
|
99
|
+
})
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
87
104
|
emitDebug({
|
|
88
105
|
type: "notificationReceivedFromHost",
|
|
89
106
|
event: parsed.message.event,
|
|
@@ -91,7 +108,7 @@ function createIframeCallRunner(options) {
|
|
|
91
108
|
});
|
|
92
109
|
return;
|
|
93
110
|
}
|
|
94
|
-
if (parsed
|
|
111
|
+
if ((parsed == null ? void 0 : parsed.type) !== "request") {
|
|
95
112
|
return;
|
|
96
113
|
}
|
|
97
114
|
if (parsed.message.cmd === "host:dispose") {
|
|
@@ -105,19 +122,21 @@ function createIframeCallRunner(options) {
|
|
|
105
122
|
);
|
|
106
123
|
});
|
|
107
124
|
function safePost(message, transfer) {
|
|
125
|
+
var _a2;
|
|
108
126
|
try {
|
|
109
127
|
transport.post(message, targetOrigin, transfer);
|
|
110
128
|
} catch (error) {
|
|
111
|
-
options.logger
|
|
129
|
+
(_a2 = options.logger) == null ? void 0 : _a2.warn("iframecall postMessage failed.", error);
|
|
112
130
|
}
|
|
113
131
|
}
|
|
114
132
|
async function handleHostDispose(reason) {
|
|
133
|
+
var _a2, _b2;
|
|
115
134
|
if (disposing || disposed) return;
|
|
116
135
|
disposing = true;
|
|
117
136
|
try {
|
|
118
|
-
await options.onHostDispose
|
|
137
|
+
await ((_a2 = options.onHostDispose) == null ? void 0 : _a2.call(options, reason));
|
|
119
138
|
} catch (error) {
|
|
120
|
-
options.logger
|
|
139
|
+
(_b2 = options.logger) == null ? void 0 : _b2.warn("iframecall host dispose handler failed.", error);
|
|
121
140
|
} finally {
|
|
122
141
|
safePost(createIframeCallNotify("terminated", { reason }));
|
|
123
142
|
disposed = true;
|
|
@@ -260,11 +279,13 @@ function getDisposeReason(payload) {
|
|
|
260
279
|
// src/iframe/useIframeCallRunner.tsx
|
|
261
280
|
import { useEffect, useRef, useState } from "react";
|
|
262
281
|
function useIframeCallRunner(options) {
|
|
282
|
+
var _a, _b;
|
|
263
283
|
const optionsRef = useRef(options);
|
|
264
284
|
optionsRef.current = options;
|
|
265
285
|
const runnerRef = useRef(null);
|
|
266
286
|
const [isActive, setIsActive] = useState(false);
|
|
267
287
|
useEffect(() => {
|
|
288
|
+
if (runnerRef.current !== null) return;
|
|
268
289
|
const opts = optionsRef.current;
|
|
269
290
|
const runner2 = createIframeCallRunner({
|
|
270
291
|
targetOrigin: opts.targetOrigin,
|
|
@@ -275,7 +296,7 @@ function useIframeCallRunner(options) {
|
|
|
275
296
|
transport: opts.transport
|
|
276
297
|
});
|
|
277
298
|
runnerRef.current = runner2;
|
|
278
|
-
setIsActive(true);
|
|
299
|
+
if (runnerRef.current === runner2) setIsActive(true);
|
|
279
300
|
let unsubscribeDebug = null;
|
|
280
301
|
const debugLog = opts.debugLog;
|
|
281
302
|
if (debugLog) {
|
|
@@ -288,13 +309,13 @@ function useIframeCallRunner(options) {
|
|
|
288
309
|
return () => {
|
|
289
310
|
if (unsubscribeDebug !== null) unsubscribeDebug();
|
|
290
311
|
runner2.dispose("react_unmount");
|
|
291
|
-
runnerRef.current = null;
|
|
312
|
+
if (runnerRef.current === runner2) runnerRef.current = null;
|
|
292
313
|
};
|
|
293
314
|
}, []);
|
|
294
315
|
const runner = runnerRef.current;
|
|
295
316
|
return {
|
|
296
|
-
commands: runner
|
|
297
|
-
iframeHelper: runner
|
|
317
|
+
commands: (_a = runner == null ? void 0 : runner.commands) != null ? _a : null,
|
|
318
|
+
iframeHelper: (_b = runner == null ? void 0 : runner.iframeHelper) != null ? _b : null,
|
|
298
319
|
isActive
|
|
299
320
|
};
|
|
300
321
|
}
|
|
@@ -18,8 +18,9 @@ type IframeCallTransport = {
|
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* host 측에서 사용하는 transport. 자식 iframe의 contentWindow로 postMessage를 보낸다.
|
|
21
|
-
* `expectedSource`는
|
|
22
|
-
* iframe src
|
|
21
|
+
* `expectedSource`는 생성 시점의 contentWindow(WindowProxy)를 캐시한다.
|
|
22
|
+
* 같은 iframe 요소의 src 변경(navigation)은 WindowProxy identity를 유지하지만,
|
|
23
|
+
* iframe 요소 자체를 교체하면 contentWindow가 달라지므로 transport도 다시 만들어야 한다.
|
|
23
24
|
*/
|
|
24
25
|
declare function createIframeWindowTransport(iframe: HTMLIFrameElement): IframeCallTransport;
|
|
25
26
|
/**
|
|
@@ -96,18 +97,38 @@ type IframeCallLogger = {
|
|
|
96
97
|
readonly info?: (message: string, detail?: unknown) => void;
|
|
97
98
|
};
|
|
98
99
|
/** host controller 생성 옵션. */
|
|
99
|
-
type IframeCallControllerOptions<TCommands extends CommandMap<TCommands>> =
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
type IframeCallControllerOptions<TCommands extends CommandMap<TCommands>> = IframeCallControllerOriginOptions & IframeCallControllerBaseOptions<TCommands>;
|
|
101
|
+
/**
|
|
102
|
+
* controller의 origin 정책 옵션.
|
|
103
|
+
* 일반 origin iframe은 targetOrigin을 명시하고, sandbox로 격리된 opaque origin("null") iframe은 opaqueOrigin으로 opt-in한다.
|
|
104
|
+
*/
|
|
105
|
+
type IframeCallControllerOriginOptions = {
|
|
106
|
+
/** opaque origin 모드를 끈다. 생략과 같다. */
|
|
107
|
+
readonly opaqueOrigin?: false;
|
|
102
108
|
/** postMessage targetOrigin. wildcard("*"/"null"/빈 문자열)는 거부한다. */
|
|
103
109
|
readonly targetOrigin: string;
|
|
104
110
|
/** 수신 시 허용할 origin 목록. 미지정이면 targetOrigin 단일 값을 사용한다. */
|
|
105
111
|
readonly allowedOrigins?: readonly string[];
|
|
112
|
+
} | {
|
|
113
|
+
/**
|
|
114
|
+
* opaque origin("null") iframe과 통신한다. 송신은 "*"로, 수신은 origin "null"이면서
|
|
115
|
+
* source가 transport.expectedSource와 같은 메시지만 받는다. expectedSource가 없으면 생성 시 거부한다.
|
|
116
|
+
*/
|
|
117
|
+
readonly opaqueOrigin: true;
|
|
118
|
+
/** opaque origin 모드에서는 지정할 수 없다. */
|
|
119
|
+
readonly targetOrigin?: never;
|
|
120
|
+
/** opaque origin 모드에서는 지정할 수 없다. */
|
|
121
|
+
readonly allowedOrigins?: never;
|
|
122
|
+
};
|
|
123
|
+
/** origin 정책을 제외한 controller 옵션. */
|
|
124
|
+
type IframeCallControllerBaseOptions<TCommands extends CommandMap<TCommands>> = {
|
|
125
|
+
/** 통신 대상 iframe element. transport 미지정 시 contentWindow 기준 기본 transport를 만든다. */
|
|
126
|
+
readonly iframe: HTMLIFrameElement;
|
|
106
127
|
/** ready 이전 호출 처리 정책. "queue"는 대기열에 쌓고, "reject"는 즉시 거부한다. */
|
|
107
128
|
readonly readyPolicy?: ReadyPolicy;
|
|
108
129
|
/** queue 정책일 때 대기열 최대 크기. 초과 호출은 queue_overflow 에러로 거부한다. */
|
|
109
130
|
readonly readyQueueLimit?: number;
|
|
110
|
-
/**
|
|
131
|
+
/** invoke() 호출별 timeout 기본값(ms). 0 또는 Infinity면 timeout을 적용하지 않는다. */
|
|
111
132
|
readonly defaultTimeoutMs?: number;
|
|
112
133
|
/** ready 신호 대기 timeout(ms). 미지정이면 defaultTimeoutMs를 따른다. */
|
|
113
134
|
readonly readyTimeoutMs?: number;
|
|
@@ -230,10 +251,12 @@ type IframeCallController<TCommands extends CommandMap<TCommands>, TNotification
|
|
|
230
251
|
/** 종료 사유를 노출하는 promise. 정상 dispose면 null, 비정상 종료면 직렬화된 에러로 resolve된다. */
|
|
231
252
|
readonly terminated: Promise<SerializedIframeCallError | null>;
|
|
232
253
|
/**
|
|
233
|
-
* iframe에 등록된 command를
|
|
234
|
-
* ready 이전
|
|
235
|
-
* timeout/transfer는
|
|
254
|
+
* iframe에 등록된 command를 실행한다.
|
|
255
|
+
* ready 이전 실행은 readyPolicy에 따라 queue되거나 즉시 거부된다.
|
|
256
|
+
* timeout/transfer는 실행별 options로 지정한다.
|
|
236
257
|
*/
|
|
258
|
+
invoke<K extends keyof TCommands & string>(cmd: K, args: CommandArgs<TCommands[K]>, options?: IframeCallCallOptions): Promise<CommandResult<TCommands[K]>>;
|
|
259
|
+
/** @deprecated `invoke()`를 사용한다. 다음 major release에서 제거될 예정이다. */
|
|
237
260
|
call<K extends keyof TCommands & string>(cmd: K, args: CommandArgs<TCommands[K]>, options?: IframeCallCallOptions): Promise<CommandResult<TCommands[K]>>;
|
|
238
261
|
/**
|
|
239
262
|
* iframe이 host로 보낸 notification을 event 이름과 payload로 구독한다.
|
|
@@ -328,4 +351,4 @@ declare function createIframeCallNotify(event: string, payload: unknown): Iframe
|
|
|
328
351
|
*/
|
|
329
352
|
declare function parseIframeCallMessage(value: unknown): ParsedIframeCallMessage | null;
|
|
330
353
|
|
|
331
|
-
export { type
|
|
354
|
+
export { type IframeCallRunnerOptions as A, type IframeCallRunnerHandle as B, type CommandMap as C, type CommandsConstructor as D, type IframeCallRunnerClassOptions as E, type IframeHelper as F, type DomainNotificationKey as G, type HostDebugEvent as H, type IframeCallControllerOptions as I, createParentWindowTransport as J, type NotifyHandler as N, type ParsedIframeCallMessage as P, type ReadyPolicy as R, type SerializedIframeCallError as S, type IframeCallController as a, type IframeCallControllerOriginOptions as b, type IframeCallControllerBaseOptions as c, type CommandArgs as d, type CommandHandler as e, type CommandResult as f, type CommandRunner as g, type IframeCallCallOptions as h, type IframeCallLogger as i, type IframeCallNotify as j, type IframeCallRequest as k, type IframeCallResponse as l, type IframeCallTransferable as m, type IframeCallTransport as n, type IframeCallTransportEvent as o, type ReservedNotificationName as p, createIframeCallError as q, createIframeCallErrorResponse as r, createIframeCallNotify as s, createIframeCallRequest as t, createIframeCallSuccessResponse as u, createIframeWindowTransport as v, isSerializedIframeCallError as w, parseIframeCallMessage as x, serializeIframeCallError as y, type IframeDebugEvent as z };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cp949/iframecall",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "타입 안전한 host ↔ iframe postMessage 호출 라이브러리 (React 18/19 지원)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iframe",
|
|
@@ -24,6 +24,10 @@
|
|
|
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",
|
|
@@ -44,23 +48,26 @@
|
|
|
44
48
|
"access": "public"
|
|
45
49
|
},
|
|
46
50
|
"peerDependencies": {
|
|
47
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
48
|
-
"
|
|
51
|
+
"@types/react": "^18.0.0 || ^19.0.0",
|
|
52
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
49
53
|
},
|
|
50
54
|
"devDependencies": {
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"@
|
|
54
|
-
"
|
|
55
|
-
"react": "
|
|
55
|
+
"@repo/eslint-config": "0.0.0",
|
|
56
|
+
"@repo/typescript-config": "0.0.0",
|
|
57
|
+
"@testing-library/dom": "^10.4.1",
|
|
58
|
+
"@testing-library/react": "^16.3.2",
|
|
59
|
+
"@types/react": "19.3.0",
|
|
60
|
+
"eslint": "^9.39.5",
|
|
61
|
+
"jsdom": "^29.1.1",
|
|
62
|
+
"react": "^19.3.0",
|
|
63
|
+
"tsup": "^8.5.1",
|
|
56
64
|
"typescript": "6.0.3",
|
|
57
|
-
"
|
|
58
|
-
"vitest": "^4.1.5",
|
|
59
|
-
"@repo/typescript-config": "0.0.0"
|
|
65
|
+
"vitest": "^4.1.11"
|
|
60
66
|
},
|
|
61
67
|
"scripts": {
|
|
62
68
|
"build": "tsup",
|
|
63
69
|
"dev": "tsup --watch",
|
|
70
|
+
"lint": "eslint . --max-warnings 0",
|
|
64
71
|
"check-types": "tsc --noEmit",
|
|
65
72
|
"test": "vitest run tests/unit --passWithNoTests"
|
|
66
73
|
}
|