@cp949/iframecall 0.1.0 → 0.2.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 +26 -0
- package/README.md +96 -31
- package/dist/host.d.ts +2 -2
- package/dist/iframe.d.ts +9 -9
- package/dist/iframe.js +24 -9
- package/dist/{messages-Cg4n93bf.d.ts → messages-BuRipoIx.d.ts} +2 -2
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
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.0] - 2026-04-28
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `$` 네임스페이스와 `$onCommandRun` wrap hook 도입 — 명령 실행 전후를 감싸는 미들웨어 형태의 훅을 등록할 수 있습니다.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- 라이프사이클/도메인 채널의 책임을 분리하고, 라이프사이클 ready 알림 API를 `sendLifecycleReady`로 정리했습니다. **(Breaking)**
|
|
16
|
+
|
|
17
|
+
### BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
- 라이프사이클 ready 전송 API의 식별자가 `sendLifecycleReady`로 리네임되었습니다. 이전 명칭을 직접 호출하던 코드는 새 이름으로 갱신해야 합니다.
|
|
20
|
+
|
|
21
|
+
## [0.1.0] - 2026-04-27
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `@cp949/iframecall` 최초 공개 — 타입 안전한 host ↔ iframe postMessage 호출 라이브러리.
|
|
26
|
+
- React 18/19 호환 진입점(`./host`, `./iframe`) 제공.
|
package/README.md
CHANGED
|
@@ -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],
|
|
@@ -104,7 +106,19 @@ export function HostPage() {
|
|
|
104
106
|
|
|
105
107
|
### 3. iframe 측 (임베드된 페이지)
|
|
106
108
|
|
|
107
|
-
커맨드 구현은 클래스로 정의한다. 생성자는 `iframeHelper`를 인자로 받으며, 메서드 이름이 곧 커맨드 이름이 된다.
|
|
109
|
+
커맨드 구현은 클래스로 정의한다. 생성자는 `iframeHelper`를 인자로 받으며, prototype에 둔 메서드 이름이 곧 커맨드 이름이 된다.
|
|
110
|
+
|
|
111
|
+
prefix 컨벤션:
|
|
112
|
+
|
|
113
|
+
| prefix | 의미 |
|
|
114
|
+
|--------|------|
|
|
115
|
+
| (없음) | host로 dispatch되는 remote command |
|
|
116
|
+
| `_` | 사용자 local-only 메서드. dispatch 대상에서 제외된다. (예: `_sendLifecycleReady`, `_onStatusChange`) |
|
|
117
|
+
| `$` | 라이브러리 점유 namespace. dispatch에서 제외되며, 라이브러리가 정의한 hook 이름만 의미가 있다. |
|
|
118
|
+
|
|
119
|
+
현재 라이브러리가 인식하는 hook은 한 개:
|
|
120
|
+
|
|
121
|
+
- `$onCommandRun(cmd, args, invoke)` — 매 command dispatch를 wrap한다. `await invoke()`를 try/finally로 감싸 상태 토글·로깅·refcount 등을 한 곳에서 처리한다.
|
|
108
122
|
|
|
109
123
|
```tsx
|
|
110
124
|
"use client";
|
|
@@ -117,14 +131,53 @@ import { useEffect } from "react";
|
|
|
117
131
|
|
|
118
132
|
const HOST_ORIGIN = "https://host.example.com";
|
|
119
133
|
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
type RunningStatus = "idle" | "processing";
|
|
135
|
+
|
|
136
|
+
// 클래스 자체가 곧 command 타입이다. 별도 interface를 두지 않는다.
|
|
137
|
+
// `_` prefix는 사용자 local-only(dispatch 제외), `$` prefix는 라이브러리 namespace.
|
|
138
|
+
class AppCommands {
|
|
139
|
+
private status: RunningStatus = "idle";
|
|
140
|
+
private inflight = 0;
|
|
141
|
+
private listeners = new Set<(s: RunningStatus) => void>();
|
|
142
|
+
|
|
143
|
+
constructor(private iframeHelper: IframeHelper<AppNotifications>) {}
|
|
144
|
+
|
|
145
|
+
_sendLifecycleReady(): void {
|
|
146
|
+
this.iframeHelper.sendLifecycleReady();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_onStatusChange(fn: (s: RunningStatus) => void): () => void {
|
|
150
|
+
this.listeners.add(fn);
|
|
151
|
+
return () => {
|
|
152
|
+
this.listeners.delete(fn);
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 모든 command를 wrap. 동시 dispatch에서 안쪽 호출이 끝나기 전에 idle로 떨어지지 않도록 refcount.
|
|
157
|
+
async $onCommandRun(
|
|
158
|
+
_cmd: string,
|
|
159
|
+
_args: readonly unknown[],
|
|
160
|
+
invoke: () => Promise<unknown>,
|
|
161
|
+
): Promise<unknown> {
|
|
162
|
+
this.inflight += 1;
|
|
163
|
+
if (this.inflight === 1) this._setStatus("processing");
|
|
164
|
+
try {
|
|
165
|
+
return await invoke();
|
|
166
|
+
} finally {
|
|
167
|
+
this.inflight -= 1;
|
|
168
|
+
if (this.inflight === 0) this._setStatus("idle");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private _setStatus(next: RunningStatus): void {
|
|
173
|
+
if (this.status === next) return;
|
|
174
|
+
this.status = next;
|
|
175
|
+
this.iframeHelper.sendNotificationToHost("status-changed", next);
|
|
176
|
+
for (const fn of this.listeners) fn(next);
|
|
177
|
+
}
|
|
122
178
|
|
|
123
179
|
async greet(name: string): Promise<string> {
|
|
124
|
-
|
|
125
|
-
const result = `Hello, ${name}!`;
|
|
126
|
-
this.iframeHelper.sendNotificationToHost("status-changed", "idle");
|
|
127
|
-
return result;
|
|
180
|
+
return `Hello, ${name}!`;
|
|
128
181
|
}
|
|
129
182
|
|
|
130
183
|
async add(a: number, b: number): Promise<number> {
|
|
@@ -132,25 +185,27 @@ class DemoCommandsImpl {
|
|
|
132
185
|
}
|
|
133
186
|
|
|
134
187
|
async delay(ms: number): Promise<void> {
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
188
|
+
await new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
191
|
|
|
139
192
|
export function IframePage() {
|
|
140
|
-
const { iframeHelper, isActive } = useIframeCallRunner<
|
|
141
|
-
|
|
142
|
-
|
|
193
|
+
const { iframeHelper, commands, isActive } = useIframeCallRunner<
|
|
194
|
+
AppCommands,
|
|
195
|
+
AppNotifications
|
|
143
196
|
>({
|
|
144
197
|
targetOrigin: HOST_ORIGIN,
|
|
145
198
|
allowedOrigins: [HOST_ORIGIN],
|
|
146
|
-
Commands:
|
|
199
|
+
Commands: AppCommands,
|
|
147
200
|
});
|
|
148
201
|
|
|
149
202
|
useEffect(() => {
|
|
150
|
-
if (!iframeHelper) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
203
|
+
if (!iframeHelper || !commands) return;
|
|
204
|
+
commands._sendLifecycleReady();
|
|
205
|
+
return commands._onStatusChange((s) => {
|
|
206
|
+
console.log("local status:", s);
|
|
207
|
+
});
|
|
208
|
+
}, [iframeHelper, commands]);
|
|
154
209
|
|
|
155
210
|
return <p>{isActive ? "active" : "initializing"}</p>;
|
|
156
211
|
}
|
|
@@ -163,21 +218,23 @@ host iframe
|
|
|
163
218
|
│ │
|
|
164
219
|
│ <iframe src="..."> │
|
|
165
220
|
│──────────────────────────────────────▶│ mount
|
|
166
|
-
│ │
|
|
167
|
-
│ ◀── ready ───────────────────────────│
|
|
221
|
+
│ │ commands._sendLifecycleReady() → sendLifecycleReady()
|
|
222
|
+
│ ◀── ready ───────────────────────────│ (lifecycle 채널)
|
|
168
223
|
│ controller.status = "ready" │
|
|
169
224
|
│ │
|
|
170
225
|
│ controller.call("greet", ["World"]) │
|
|
171
|
-
│ ── request ─────────────────────────▶│
|
|
226
|
+
│ ── request ─────────────────────────▶│ $onCommandRun → AppCommands.greet("World")
|
|
227
|
+
│ ◀── notify status-changed:processing │ (도메인 채널, refcount 0→1)
|
|
172
228
|
│ ◀── response: "Hello, World!" ───────│
|
|
173
|
-
│
|
|
174
|
-
│ ◀── notify "status-changed" ─────────│ sendNotificationToHost(...)
|
|
229
|
+
│ ◀── notify status-changed:idle ──────│ (도메인 채널, refcount 1→0)
|
|
175
230
|
```
|
|
176
231
|
|
|
177
|
-
- iframe이 마운트되면 `
|
|
178
|
-
- host의 `controller.call`은 ready 시점까지 대기한 뒤
|
|
232
|
+
- iframe이 마운트되면 `commands._sendLifecycleReady()`가 `sendLifecycleReady()`를 통해 transport ready 신호를 보낸다.
|
|
233
|
+
- host의 `controller.call`은 ready 시점까지 대기한 뒤 전송된다.
|
|
179
234
|
- 응답은 Promise로 돌아오며, iframe 측 메서드가 throw하면 host 쪽 Promise는 reject된다.
|
|
180
235
|
- iframe → host 단방향 알림은 `sendNotificationToHost`로 보내고, host 쪽에서 `controller.onNotificationFromIframe`으로 받는다.
|
|
236
|
+
- **라이프사이클 채널과 도메인 채널은 책임이 다르다.** `ready`/`terminated`는 transport 신호 전용이고, 도메인 알림(`status-changed` 등)에는 `"ready"` 같은 lifecycle 의미를 담지 않는다.
|
|
237
|
+
- prototype에 `$onCommandRun(cmd, args, invoke)`을 두면 모든 command dispatch가 그 함수로 wrap된다. status 토글·로깅·refcount 같은 횡단 관심사를 한 곳에서 처리할 수 있다.
|
|
181
238
|
|
|
182
239
|
## API 개요
|
|
183
240
|
|
|
@@ -203,16 +260,24 @@ host iframe
|
|
|
203
260
|
|
|
204
261
|
| export | 종류 | 설명 |
|
|
205
262
|
|--------|------|------|
|
|
206
|
-
| `useIframeCallRunner` | hook | iframe용 React 훅. `iframeHelper`, `isActive`를 반환한다. |
|
|
263
|
+
| `useIframeCallRunner` | hook | iframe용 React 훅. `commands`, `iframeHelper`, `isActive`를 반환한다. mount 전 `commands`/`iframeHelper`는 `null`. |
|
|
207
264
|
| `createIframeCallRunner` | factory | 훅 없이 러너를 직접 만들 때 사용 |
|
|
208
265
|
| `createParentWindowTransport` | factory | 커스텀 트랜스포트 구성용 |
|
|
209
266
|
|
|
210
267
|
`iframeHelper`의 주요 멤버:
|
|
211
268
|
|
|
212
|
-
- `iframeHelper.sendNotificationToHost(event, payload)` — host로 알림 전송 (lifecycle 예약 이름 `ready`/`terminated`는 제외)
|
|
213
|
-
- `iframeHelper.
|
|
269
|
+
- `iframeHelper.sendNotificationToHost(event, payload)` — host로 도메인 알림 전송 (lifecycle 예약 이름 `ready`/`terminated`는 타입에서 제외)
|
|
270
|
+
- `iframeHelper.sendLifecycleReady()` — host에 transport lifecycle ready 신호 전송. 도메인 알림과는 채널이 다르다.
|
|
214
271
|
- `iframeHelper.debug.subscribe(handler)` — 디버그 스트림 구독
|
|
215
272
|
|
|
273
|
+
**Commands class prefix 컨벤션:**
|
|
274
|
+
|
|
275
|
+
- prefix 없는 prototype 메서드 → host에서 호출 가능한 remote command
|
|
276
|
+
- `_` prefix → 사용자 local-only (dispatch 제외)
|
|
277
|
+
- `$` prefix → 라이브러리 namespace (dispatch 제외, 인식되는 hook은 `$onCommandRun`)
|
|
278
|
+
|
|
279
|
+
`onStatusChange`처럼 prefix가 없으면서 host에서 호출하면 안 되는 메서드는 반드시 `_` prefix를 붙여야 한다. 그렇지 않으면 host가 `controller.call("onStatusChange", [...])`로 직접 호출할 수 있다.
|
|
280
|
+
|
|
216
281
|
> 알림은 iframe → host 단방향이다. host → iframe 알림은 라이브러리 외부에서 `postMessage`로 직접 처리하거나, host에서 커맨드를 호출해 처리한다.
|
|
217
282
|
|
|
218
283
|
### 공통 타입
|
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-BuRipoIx.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-BuRipoIx.js';
|
|
3
3
|
|
|
4
4
|
/** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:host]가 기본값이다. */
|
|
5
5
|
type ConsoleDebugLoggerOptions = {
|
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-BuRipoIx.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-BuRipoIx.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
|
@@ -59,7 +59,7 @@ function createIframeCallRunner(options) {
|
|
|
59
59
|
safePost(createIframeCallNotify(event, payload));
|
|
60
60
|
emitDebug({ type: "notificationSentToHost", event, payload });
|
|
61
61
|
},
|
|
62
|
-
|
|
62
|
+
sendLifecycleReady() {
|
|
63
63
|
if (disposing || disposed) return;
|
|
64
64
|
safePost(createIframeCallNotify("ready", { protocolVersion: 1 }));
|
|
65
65
|
},
|
|
@@ -73,7 +73,7 @@ function createIframeCallRunner(options) {
|
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
75
|
const iframeHelper = helperInternal;
|
|
76
|
-
const { commands, dispatch } = resolveCommandSource(options, iframeHelper);
|
|
76
|
+
const { commands, dispatch, commandRunHook } = resolveCommandSource(options, iframeHelper);
|
|
77
77
|
const unsubscribeTransport = transport.subscribe((event) => {
|
|
78
78
|
if (disposing || disposed) return;
|
|
79
79
|
if (!allowedOrigins.has(event.origin)) {
|
|
@@ -139,7 +139,8 @@ function createIframeCallRunner(options) {
|
|
|
139
139
|
return;
|
|
140
140
|
}
|
|
141
141
|
try {
|
|
142
|
-
const
|
|
142
|
+
const invoke = () => Promise.resolve(handler(...args));
|
|
143
|
+
const value = commandRunHook ? await commandRunHook(cmd, args, invoke) : await invoke();
|
|
143
144
|
if (disposing || disposed) return;
|
|
144
145
|
safePost(createIframeCallSuccessResponse(id, value));
|
|
145
146
|
emitDebug({ type: "commandResultSentToHost", command: cmd, value });
|
|
@@ -159,8 +160,8 @@ function createIframeCallRunner(options) {
|
|
|
159
160
|
commands,
|
|
160
161
|
iframeHelper,
|
|
161
162
|
sendNotificationToHost: sendNotificationToHostUntyped,
|
|
162
|
-
|
|
163
|
-
iframeHelper.
|
|
163
|
+
sendLifecycleReady() {
|
|
164
|
+
iframeHelper.sendLifecycleReady();
|
|
164
165
|
},
|
|
165
166
|
terminated(reason, error) {
|
|
166
167
|
if (disposing || disposed) return;
|
|
@@ -197,11 +198,13 @@ function resolveCommandSource(options, iframeHelper) {
|
|
|
197
198
|
const Ctor = rawOptions.Commands;
|
|
198
199
|
const instance = new Ctor(iframeHelper);
|
|
199
200
|
const handlerCache = buildPrototypeCommandHandlers(instance);
|
|
201
|
+
const commandRunHook = resolveCommandRunHook(instance);
|
|
200
202
|
return {
|
|
201
203
|
commands: instance,
|
|
202
204
|
dispatch(cmd) {
|
|
203
205
|
return handlerCache.get(cmd);
|
|
204
|
-
}
|
|
206
|
+
},
|
|
207
|
+
commandRunHook
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
210
|
function buildPrototypeCommandHandlers(instance) {
|
|
@@ -212,6 +215,7 @@ function buildPrototypeCommandHandlers(instance) {
|
|
|
212
215
|
if (typeof key !== "string") continue;
|
|
213
216
|
if (RESERVED_COMMAND_NAMES.has(key)) continue;
|
|
214
217
|
if (key.startsWith("_")) continue;
|
|
218
|
+
if (key.startsWith("$")) continue;
|
|
215
219
|
if (handlers.has(key)) continue;
|
|
216
220
|
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
|
|
217
221
|
if (descriptor === void 0) continue;
|
|
@@ -225,6 +229,18 @@ function buildPrototypeCommandHandlers(instance) {
|
|
|
225
229
|
}
|
|
226
230
|
return handlers;
|
|
227
231
|
}
|
|
232
|
+
function resolveCommandRunHook(instance) {
|
|
233
|
+
let proto = Object.getPrototypeOf(instance);
|
|
234
|
+
while (proto !== null && proto !== Object.prototype) {
|
|
235
|
+
const descriptor = Object.getOwnPropertyDescriptor(proto, "$onCommandRun");
|
|
236
|
+
if (descriptor !== void 0 && typeof descriptor.value === "function") {
|
|
237
|
+
const hook = descriptor.value;
|
|
238
|
+
return hook.bind(instance);
|
|
239
|
+
}
|
|
240
|
+
proto = Object.getPrototypeOf(proto);
|
|
241
|
+
}
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
228
244
|
function requireTargetOrigin(targetOrigin) {
|
|
229
245
|
if (targetOrigin.length === 0 || targetOrigin === "*" || targetOrigin === "null") {
|
|
230
246
|
throw createIframeCallError(
|
|
@@ -277,9 +293,8 @@ function useIframeCallRunner(options) {
|
|
|
277
293
|
}, []);
|
|
278
294
|
const runner = runnerRef.current;
|
|
279
295
|
return {
|
|
280
|
-
commands: runner?.commands,
|
|
281
|
-
iframeHelper: runner?.iframeHelper,
|
|
282
|
-
runner: runner ?? void 0,
|
|
296
|
+
commands: runner?.commands ?? null,
|
|
297
|
+
iframeHelper: runner?.iframeHelper ?? null,
|
|
283
298
|
isActive
|
|
284
299
|
};
|
|
285
300
|
}
|
|
@@ -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;
|
|
@@ -272,7 +272,7 @@ type IframeCallRunnerHandle<TCommands = Record<string, CommandHandler>, TNotific
|
|
|
272
272
|
/** iframeHelper.sendNotificationToHost와 동일한 동작을 runner handle에서 노출한다. */
|
|
273
273
|
sendNotificationToHost: IsWildcardNotificationMap<TNotificationsToHost> extends true ? (event: string, payload: unknown) => void : <K extends DomainNotificationKey<TNotificationsToHost>>(event: K, payload: TNotificationsToHost[K]) => void;
|
|
274
274
|
/** transport lifecycle ready 신호. payload는 `{ protocolVersion: 1 }`로 고정된다. */
|
|
275
|
-
|
|
275
|
+
sendLifecycleReady(): void;
|
|
276
276
|
terminated(reason: string, error?: SerializedIframeCallError): void;
|
|
277
277
|
/** local transport subscription을 즉시 정리하고 이후 inbound/outbound를 모두 no-op으로 만든다. */
|
|
278
278
|
dispose(reason?: string): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cp949/iframecall",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "타입 안전한 host ↔ iframe postMessage 호출 라이브러리 (React 18/19 지원)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iframe",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
32
33
|
"exports": {
|