@cp949/iframecall 0.1.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/dist/iframe.js ADDED
@@ -0,0 +1,299 @@
1
+ import {
2
+ createIframeCallError,
3
+ createIframeCallErrorResponse,
4
+ createIframeCallNotify,
5
+ createIframeCallRequest,
6
+ createIframeCallSuccessResponse,
7
+ createParentWindowTransport,
8
+ isSerializedIframeCallError,
9
+ parseIframeCallMessage,
10
+ serializeIframeCallError
11
+ } from "./chunk-UYZXYOI6.js";
12
+
13
+ // src/iframe/consoleDebugLogger.ts
14
+ function consoleDebugLogger(options = {}) {
15
+ const prefix = options.prefix ?? "[iframecall:iframe]";
16
+ const head = prefix.length > 0 ? `${prefix} ` : "";
17
+ return (event) => {
18
+ switch (event.type) {
19
+ case "commandReceivedFromHost":
20
+ console.debug(`${head}${event.type} ${event.command}`, event.args);
21
+ return;
22
+ case "commandResultSentToHost":
23
+ console.debug(`${head}${event.type} ${event.command}`, event.value);
24
+ return;
25
+ case "commandErrorSentToHost":
26
+ console.debug(`${head}${event.type} ${event.command}`, event.error);
27
+ return;
28
+ case "notificationSentToHost":
29
+ console.debug(`${head}${event.type} ${event.event}`, event.payload);
30
+ return;
31
+ case "notificationReceivedFromHost":
32
+ console.debug(`${head}${event.type} ${event.event}`, event.payload);
33
+ return;
34
+ }
35
+ };
36
+ }
37
+
38
+ // src/iframe/runner.ts
39
+ var RESERVED_COMMAND_NAMES = /* @__PURE__ */ new Set(["constructor", "host:dispose"]);
40
+ function createIframeCallRunner(options) {
41
+ const targetOrigin = requireTargetOrigin(options.targetOrigin);
42
+ const allowedOrigins = new Set(options.allowedOrigins ?? [targetOrigin]);
43
+ const transport = options.transport ?? createParentWindowTransport();
44
+ let disposing = false;
45
+ let disposed = false;
46
+ const debugSubscribers = /* @__PURE__ */ new Set();
47
+ function emitDebug(event) {
48
+ for (const handler of debugSubscribers) {
49
+ try {
50
+ handler(event);
51
+ } catch (error) {
52
+ options.logger?.warn("iframecall debug subscriber threw.", error);
53
+ }
54
+ }
55
+ }
56
+ const helperInternal = {
57
+ sendNotificationToHost(event, payload) {
58
+ if (disposing || disposed) return;
59
+ safePost(createIframeCallNotify(event, payload));
60
+ emitDebug({ type: "notificationSentToHost", event, payload });
61
+ },
62
+ sendReadyToHost() {
63
+ if (disposing || disposed) return;
64
+ safePost(createIframeCallNotify("ready", { protocolVersion: 1 }));
65
+ },
66
+ debug: {
67
+ subscribe(handler) {
68
+ debugSubscribers.add(handler);
69
+ return () => {
70
+ debugSubscribers.delete(handler);
71
+ };
72
+ }
73
+ }
74
+ };
75
+ const iframeHelper = helperInternal;
76
+ const { commands, dispatch } = resolveCommandSource(options, iframeHelper);
77
+ const unsubscribeTransport = transport.subscribe((event) => {
78
+ if (disposing || disposed) return;
79
+ if (!allowedOrigins.has(event.origin)) {
80
+ return;
81
+ }
82
+ if (transport.expectedSource !== void 0 && event.source !== transport.expectedSource) {
83
+ return;
84
+ }
85
+ const parsed = parseIframeCallMessage(event.data);
86
+ if (parsed?.type === "notify") {
87
+ emitDebug({
88
+ type: "notificationReceivedFromHost",
89
+ event: parsed.message.event,
90
+ payload: parsed.message.payload
91
+ });
92
+ return;
93
+ }
94
+ if (parsed?.type !== "request") {
95
+ return;
96
+ }
97
+ if (parsed.message.cmd === "host:dispose") {
98
+ void handleHostDispose(getDisposeReason(parsed.message.args[0]));
99
+ return;
100
+ }
101
+ void handleRequest(
102
+ parsed.message.id,
103
+ parsed.message.cmd,
104
+ parsed.message.args
105
+ );
106
+ });
107
+ function safePost(message, transfer) {
108
+ try {
109
+ transport.post(message, targetOrigin, transfer);
110
+ } catch (error) {
111
+ options.logger?.warn("iframecall postMessage failed.", error);
112
+ }
113
+ }
114
+ async function handleHostDispose(reason) {
115
+ if (disposing || disposed) return;
116
+ disposing = true;
117
+ try {
118
+ await options.onHostDispose?.(reason);
119
+ } catch (error) {
120
+ options.logger?.warn("iframecall host dispose handler failed.", error);
121
+ } finally {
122
+ safePost(createIframeCallNotify("terminated", { reason }));
123
+ disposed = true;
124
+ unsubscribeTransport();
125
+ }
126
+ }
127
+ async function handleRequest(id, cmd, args) {
128
+ if (disposing || disposed) return;
129
+ emitDebug({ type: "commandReceivedFromHost", command: cmd, args });
130
+ const handler = dispatch(cmd);
131
+ if (handler === void 0) {
132
+ const error = createIframeCallError(
133
+ "command_not_found",
134
+ `Command not found: ${cmd}`,
135
+ { command: cmd }
136
+ );
137
+ safePost(createIframeCallErrorResponse(id, error));
138
+ emitDebug({ type: "commandErrorSentToHost", command: cmd, error });
139
+ return;
140
+ }
141
+ try {
142
+ const value = await handler(...args);
143
+ if (disposing || disposed) return;
144
+ safePost(createIframeCallSuccessResponse(id, value));
145
+ emitDebug({ type: "commandResultSentToHost", command: cmd, value });
146
+ } catch (rawError) {
147
+ if (disposing || disposed) return;
148
+ const serialized = serializeIframeCallError(rawError, cmd);
149
+ safePost(createIframeCallErrorResponse(id, serialized));
150
+ emitDebug({
151
+ type: "commandErrorSentToHost",
152
+ command: cmd,
153
+ error: serialized
154
+ });
155
+ }
156
+ }
157
+ const sendNotificationToHostUntyped = (event, payload) => helperInternal.sendNotificationToHost(event, payload);
158
+ const handle = {
159
+ commands,
160
+ iframeHelper,
161
+ sendNotificationToHost: sendNotificationToHostUntyped,
162
+ sendReadyToHost() {
163
+ iframeHelper.sendReadyToHost();
164
+ },
165
+ terminated(reason, error) {
166
+ if (disposing || disposed) return;
167
+ disposing = true;
168
+ safePost(createIframeCallNotify("terminated", { reason, error }));
169
+ disposed = true;
170
+ unsubscribeTransport();
171
+ },
172
+ dispose(_reason) {
173
+ if (disposing || disposed) return;
174
+ disposing = true;
175
+ disposed = true;
176
+ unsubscribeTransport();
177
+ }
178
+ };
179
+ return handle;
180
+ }
181
+ function resolveCommandSource(options, iframeHelper) {
182
+ const rawOptions = options;
183
+ const hasCommandsObject = "commands" in rawOptions && rawOptions.commands !== void 0;
184
+ const hasCommandsClass = "Commands" in rawOptions && rawOptions.Commands !== void 0;
185
+ if (hasCommandsObject) {
186
+ throw createIframeCallError(
187
+ "invalid_args",
188
+ "createIframeCallRunner no longer accepts { commands }. Use { Commands } class."
189
+ );
190
+ }
191
+ if (!hasCommandsClass) {
192
+ throw createIframeCallError(
193
+ "invalid_args",
194
+ "createIframeCallRunner requires { Commands } class."
195
+ );
196
+ }
197
+ const Ctor = rawOptions.Commands;
198
+ const instance = new Ctor(iframeHelper);
199
+ const handlerCache = buildPrototypeCommandHandlers(instance);
200
+ return {
201
+ commands: instance,
202
+ dispatch(cmd) {
203
+ return handlerCache.get(cmd);
204
+ }
205
+ };
206
+ }
207
+ function buildPrototypeCommandHandlers(instance) {
208
+ const handlers = /* @__PURE__ */ new Map();
209
+ let proto = Object.getPrototypeOf(instance);
210
+ while (proto !== null && proto !== Object.prototype) {
211
+ for (const key of Reflect.ownKeys(proto)) {
212
+ if (typeof key !== "string") continue;
213
+ if (RESERVED_COMMAND_NAMES.has(key)) continue;
214
+ if (key.startsWith("_")) continue;
215
+ if (handlers.has(key)) continue;
216
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
217
+ if (descriptor === void 0) continue;
218
+ if (descriptor.get !== void 0 || descriptor.set !== void 0)
219
+ continue;
220
+ if (typeof descriptor.value !== "function") continue;
221
+ const method = descriptor.value;
222
+ handlers.set(key, method.bind(instance));
223
+ }
224
+ proto = Object.getPrototypeOf(proto);
225
+ }
226
+ return handlers;
227
+ }
228
+ function requireTargetOrigin(targetOrigin) {
229
+ if (targetOrigin.length === 0 || targetOrigin === "*" || targetOrigin === "null") {
230
+ throw createIframeCallError(
231
+ "invalid_origin",
232
+ "targetOrigin must be explicit."
233
+ );
234
+ }
235
+ return targetOrigin;
236
+ }
237
+ function getDisposeReason(payload) {
238
+ if (typeof payload === "object" && payload !== null && "reason" in payload && typeof payload.reason === "string") {
239
+ return payload.reason;
240
+ }
241
+ return "host_requested";
242
+ }
243
+
244
+ // src/iframe/useIframeCallRunner.tsx
245
+ import { useEffect, useRef, useState } from "react";
246
+ function useIframeCallRunner(options) {
247
+ const optionsRef = useRef(options);
248
+ optionsRef.current = options;
249
+ const runnerRef = useRef(null);
250
+ const [isActive, setIsActive] = useState(false);
251
+ useEffect(() => {
252
+ const opts = optionsRef.current;
253
+ const runner2 = createIframeCallRunner({
254
+ targetOrigin: opts.targetOrigin,
255
+ allowedOrigins: opts.allowedOrigins,
256
+ Commands: opts.Commands,
257
+ logger: opts.logger,
258
+ onHostDispose: opts.onHostDispose,
259
+ transport: opts.transport
260
+ });
261
+ runnerRef.current = runner2;
262
+ setIsActive(true);
263
+ let unsubscribeDebug = null;
264
+ const debugLog = opts.debugLog;
265
+ if (debugLog) {
266
+ const prefix = typeof debugLog === "object" && debugLog !== null ? debugLog.prefix : void 0;
267
+ const handler = consoleDebugLogger(
268
+ prefix !== void 0 ? { prefix } : void 0
269
+ );
270
+ unsubscribeDebug = runner2.iframeHelper.debug.subscribe(handler);
271
+ }
272
+ return () => {
273
+ if (unsubscribeDebug !== null) unsubscribeDebug();
274
+ runner2.dispose("react_unmount");
275
+ runnerRef.current = null;
276
+ };
277
+ }, []);
278
+ const runner = runnerRef.current;
279
+ return {
280
+ commands: runner?.commands,
281
+ iframeHelper: runner?.iframeHelper,
282
+ runner: runner ?? void 0,
283
+ isActive
284
+ };
285
+ }
286
+ export {
287
+ consoleDebugLogger,
288
+ createIframeCallError,
289
+ createIframeCallErrorResponse,
290
+ createIframeCallNotify,
291
+ createIframeCallRequest,
292
+ createIframeCallRunner,
293
+ createIframeCallSuccessResponse,
294
+ createParentWindowTransport,
295
+ isSerializedIframeCallError,
296
+ parseIframeCallMessage,
297
+ serializeIframeCallError,
298
+ useIframeCallRunner
299
+ };
@@ -0,0 +1,331 @@
1
+ /** transport가 controller/runner로 전달하는 정규화된 수신 이벤트. */
2
+ type IframeCallTransportEvent = {
3
+ /** postMessage로 도착한 raw payload. 파싱 전 단계의 값이다. */
4
+ readonly data: unknown;
5
+ /** 메시지 송신자의 origin. allowedOrigins 검증에 사용한다. */
6
+ readonly origin: string;
7
+ /** 송신자 Window 참조. expectedSource와 비교해 cross-frame 위장을 차단한다. */
8
+ readonly source: unknown;
9
+ };
10
+ /** controller/runner가 사용하는 transport 추상화. Window 외 다른 매체로도 교체할 수 있다. */
11
+ type IframeCallTransport = {
12
+ /** 송신자 Window의 기대값. 정의되어 있으면 수신 시 source 일치를 강제한다. */
13
+ expectedSource?: unknown;
14
+ /** 메시지를 targetOrigin으로 전송한다. transferable이 있으면 ownership을 함께 넘긴다. */
15
+ post(message: unknown, targetOrigin: string, transfer?: readonly IframeCallTransferable[]): void;
16
+ /** 수신 이벤트를 구독한다. 반환된 함수를 호출하면 listener를 해제한다. */
17
+ subscribe(handler: (event: IframeCallTransportEvent) => void): () => void;
18
+ };
19
+ /**
20
+ * host 측에서 사용하는 transport. 자식 iframe의 contentWindow로 postMessage를 보낸다.
21
+ * `expectedSource`는 contentWindow가 swap되기 전 시점에 캐시되므로,
22
+ * iframe src 변경처럼 contentWindow가 교체되는 경우 transport도 다시 만들어야 한다.
23
+ */
24
+ declare function createIframeWindowTransport(iframe: HTMLIFrameElement): IframeCallTransport;
25
+ /**
26
+ * iframe 측에서 사용하는 transport. 부모 Window를 송신 대상으로 삼고,
27
+ * 수신 이벤트의 source가 부모 Window 참조와 일치하는지 검증한다.
28
+ */
29
+ declare function createParentWindowTransport(): IframeCallTransport;
30
+
31
+ /** Wire envelope: host -> iframe 방향 request. */
32
+ type IframeCallRequest = {
33
+ readonly protocol: "iframecall";
34
+ readonly version: 1;
35
+ readonly id: string;
36
+ readonly cmd: string;
37
+ readonly args: readonly unknown[];
38
+ };
39
+ /** Wire envelope: iframe -> host 방향 response. 성공/실패 모두 포함한다. */
40
+ type IframeCallResponse = {
41
+ readonly protocol: "iframecall";
42
+ readonly version: 1;
43
+ readonly id: string;
44
+ readonly ok: true;
45
+ readonly value: unknown;
46
+ } | {
47
+ readonly protocol: "iframecall";
48
+ readonly version: 1;
49
+ readonly id: string;
50
+ readonly ok: false;
51
+ readonly error: SerializedIframeCallError;
52
+ };
53
+ /** Wire envelope: iframe -> host 방향 notify. id 없이 event 이름과 payload만 가진다. */
54
+ type IframeCallNotify = {
55
+ readonly protocol: "iframecall";
56
+ readonly version: 1;
57
+ readonly event: string;
58
+ readonly payload: unknown;
59
+ };
60
+ /** Error response body. 재귀적으로 cause를 가질 수 있다. */
61
+ type SerializedIframeCallError = {
62
+ readonly code: string;
63
+ readonly message: string;
64
+ readonly command?: string;
65
+ readonly details?: unknown;
66
+ readonly cause?: SerializedIframeCallError;
67
+ };
68
+ /** Host controller가 ready 이전 호출을 어떻게 다룰지 선택한다. */
69
+ type ReadyPolicy = "queue" | "reject";
70
+ /** command handler 시그니처. args와 return 모두 structured clone 가능해야 한다. */
71
+ type CommandHandler<TArgs extends readonly unknown[] = readonly unknown[], TResult = unknown> = (...args: TArgs) => TResult | PromiseLike<TResult>;
72
+ /** command map의 각 property가 command handler인지 검증한다. */
73
+ type CommandMap<TCommands> = {
74
+ readonly [K in keyof TCommands]: TCommands[K] extends (...args: infer TArgs) => infer TResult ? TArgs extends readonly unknown[] ? CommandHandler<TArgs, Awaited<TResult>> : never : never;
75
+ };
76
+ /** command handler의 args tuple을 꺼낸다. */
77
+ type CommandArgs<TCommand> = TCommand extends (...args: infer TArgs) => unknown ? TArgs : never;
78
+ /** command handler의 awaited return type을 꺼낸다. */
79
+ type CommandResult<TCommand> = TCommand extends (...args: infer TArgs) => infer TResult ? TArgs extends readonly unknown[] ? Awaited<TResult> : never : never;
80
+ /** 도메인별 command 목록을 타입 파라미터로 받는 command map. */
81
+ type CommandRunner<TCommands extends CommandMap<TCommands>> = {
82
+ readonly commands: TCommands;
83
+ };
84
+ /** host가 subscribe하는 notify handler. */
85
+ type NotifyHandler<TPayload = unknown> = (payload: TPayload) => void;
86
+ /** postMessage가 ownership을 넘길 transferable 값. */
87
+ type IframeCallTransferable = Transferable;
88
+ /** command 호출별 timeout과 transfer 대상을 지정한다. */
89
+ type IframeCallCallOptions = {
90
+ readonly timeoutMs?: number;
91
+ readonly transfer?: readonly IframeCallTransferable[];
92
+ };
93
+ /** 디버그용 최소 logger. */
94
+ type IframeCallLogger = {
95
+ readonly warn: (message: string, detail?: unknown) => void;
96
+ readonly info?: (message: string, detail?: unknown) => void;
97
+ };
98
+ /** host controller 생성 옵션. */
99
+ type IframeCallControllerOptions<TCommands extends CommandMap<TCommands>> = {
100
+ /** 통신 대상 iframe element. transport 미지정 시 contentWindow 기준 기본 transport를 만든다. */
101
+ readonly iframe: HTMLIFrameElement;
102
+ /** postMessage targetOrigin. wildcard("*"/"null"/빈 문자열)는 거부한다. */
103
+ readonly targetOrigin: string;
104
+ /** 수신 시 허용할 origin 목록. 미지정이면 targetOrigin 단일 값을 사용한다. */
105
+ readonly allowedOrigins?: readonly string[];
106
+ /** ready 이전 호출 처리 정책. "queue"는 대기열에 쌓고, "reject"는 즉시 거부한다. */
107
+ readonly readyPolicy?: ReadyPolicy;
108
+ /** queue 정책일 때 대기열 최대 크기. 초과 호출은 queue_overflow 에러로 거부한다. */
109
+ readonly readyQueueLimit?: number;
110
+ /** call() 호출별 timeout 기본값(ms). 0 또는 Infinity면 timeout을 적용하지 않는다. */
111
+ readonly defaultTimeoutMs?: number;
112
+ /** ready 신호 대기 timeout(ms). 미지정이면 defaultTimeoutMs를 따른다. */
113
+ readonly readyTimeoutMs?: number;
114
+ /** request id 생성기. 테스트 환경에서 결정적인 id를 주입할 때 사용한다. */
115
+ readonly generateId?: () => string;
116
+ /** 디버그 로그 출력 hook. ready 중복 수신, postMessage 실패 등 비치명적 이벤트만 흘려준다. */
117
+ readonly logger?: IframeCallLogger;
118
+ /** Window 외 매체로 통신할 때 주입하는 transport. 미지정이면 iframe 기반 transport를 사용한다. */
119
+ readonly transport?: IframeCallTransport;
120
+ /** TCommands를 추론에 강제하기 위한 phantom 필드. 런타임에서는 사용하지 않는다. */
121
+ readonly __commandsPhantom?: TCommands;
122
+ };
123
+ /**
124
+ * `ready`는 transport lifecycle event라 domain notification map에 섞이지 않는다.
125
+ * 사용자가 `TNotificationsToHost`에 `ready` 키를 넣으려 해도 type-level에서 거부한다.
126
+ */
127
+ type ReservedNotificationName = "ready" | "terminated";
128
+ /**
129
+ * 도메인 notification map에서 lifecycle 예약 이름을 제거한다.
130
+ * `sendNotificationToHost`의 generic K 추론에 사용한다.
131
+ */
132
+ type DomainNotificationKey<TNotificationsToHost> = Exclude<keyof TNotificationsToHost & string, ReservedNotificationName>;
133
+ /**
134
+ * iframe runner가 host로 보낼 수 있는 notification helper.
135
+ * Commands class constructor에 주입되어 도메인 코드가 host로 신호를 흘려보낼 때 사용한다.
136
+ *
137
+ * 명시된 notification map에서는 lifecycle 예약 이름을 generic K에서 자동으로 제거하고,
138
+ * default wildcard map에서는 string event를 받는다.
139
+ */
140
+ type IframeHelper<TNotificationsToHost = Record<string, unknown>> = {
141
+ /** 도메인 notification을 host로 전송한다. lifecycle 예약 이름은 받지 않는다. */
142
+ sendNotificationToHost: IsWildcardNotificationMap<TNotificationsToHost> extends true ? (event: string, payload: unknown) => void : <K extends DomainNotificationKey<TNotificationsToHost>>(event: K, payload: TNotificationsToHost[K]) => void;
143
+ /** transport lifecycle ready 신호를 host로 전송한다. payload는 라이브러리가 고정한다. */
144
+ sendReadyToHost(): void;
145
+ /** 개발/디버그 패널이 iframecall 통신 흐름을 관찰할 수 있도록 raw event를 흘려준다. */
146
+ readonly debug: {
147
+ subscribe(handler: (event: IframeDebugEvent) => void): () => void;
148
+ };
149
+ };
150
+ /**
151
+ * 디버그 패널이 관찰하는 iframecall 통신 이벤트.
152
+ * raw payload를 그대로 전달하므로 production logging에는 사용하지 않는다.
153
+ */
154
+ type IframeDebugEvent = {
155
+ readonly type: "commandReceivedFromHost";
156
+ readonly command: string;
157
+ readonly args: readonly unknown[];
158
+ } | {
159
+ readonly type: "commandResultSentToHost";
160
+ readonly command: string;
161
+ readonly value: unknown;
162
+ } | {
163
+ readonly type: "commandErrorSentToHost";
164
+ readonly command: string;
165
+ readonly error: SerializedIframeCallError;
166
+ } | {
167
+ readonly type: "notificationSentToHost";
168
+ readonly event: string;
169
+ readonly payload: unknown;
170
+ } | {
171
+ readonly type: "notificationReceivedFromHost";
172
+ readonly event: string;
173
+ readonly payload: unknown;
174
+ };
175
+ /**
176
+ * host controller가 관찰하는 iframecall 통신 이벤트.
177
+ * raw payload를 그대로 전달하므로 production logging에는 사용하지 않는다.
178
+ * iframe 측 IframeDebugEvent와 같은 dev-only 정책을 따른다.
179
+ */
180
+ type HostDebugEvent = {
181
+ readonly type: "commandSentToIframe";
182
+ readonly command: string;
183
+ readonly args: readonly unknown[];
184
+ } | {
185
+ readonly type: "commandResultReceivedFromIframe";
186
+ readonly command: string;
187
+ readonly value: unknown;
188
+ } | {
189
+ readonly type: "commandErrorReceivedFromIframe";
190
+ readonly command: string;
191
+ readonly error: SerializedIframeCallError;
192
+ } | {
193
+ readonly type: "notificationReceivedFromIframe";
194
+ readonly event: string;
195
+ readonly payload: unknown;
196
+ } | {
197
+ readonly type: "readyReceived";
198
+ readonly payload: unknown;
199
+ } | {
200
+ readonly type: "terminatedReceived";
201
+ readonly reason: string;
202
+ readonly error: SerializedIframeCallError | null;
203
+ };
204
+ /**
205
+ * iframe 업체가 구현하는 `Commands` class의 constructor 시그니처.
206
+ * runner가 `new Commands(iframeHelper)`로 정확히 한 번 호출한다.
207
+ */
208
+ type CommandsConstructor<TCommands, TNotificationsToHost = Record<string, unknown>> = new (iframeHelper: IframeHelper<TNotificationsToHost>) => TCommands;
209
+ /**
210
+ * class 기반 runner 옵션.
211
+ * 업체 개발자는 `Commands` class만 작성하면 되고, runner가 `iframeHelper`를 주입한다.
212
+ */
213
+ type IframeCallRunnerClassOptions<TCommands, TNotificationsToHost = Record<string, unknown>> = {
214
+ readonly targetOrigin: string;
215
+ readonly allowedOrigins?: readonly string[];
216
+ readonly Commands: CommandsConstructor<TCommands, TNotificationsToHost>;
217
+ readonly logger?: IframeCallLogger;
218
+ readonly onHostDispose?: (reason: string) => void | PromiseLike<void>;
219
+ readonly transport?: IframeCallTransport;
220
+ };
221
+ /** iframe runner 생성 옵션. */
222
+ type IframeCallRunnerOptions<TCommands, TNotificationsToHost = Record<string, unknown>> = IframeCallRunnerClassOptions<TCommands, TNotificationsToHost>;
223
+ /**
224
+ * host controller public surface.
225
+ * 두 번째 generic은 default를 두어 단일 generic 호출처가 typecheck를 유지한다.
226
+ */
227
+ type IframeCallController<TCommands extends CommandMap<TCommands>, TNotificationsFromIframe = Record<string, unknown>> = {
228
+ /** iframe이 ready 신호를 보낼 때까지 대기하는 promise. terminate 시 reject된다. */
229
+ readonly ready: Promise<void>;
230
+ /** 종료 사유를 노출하는 promise. 정상 dispose면 null, 비정상 종료면 직렬화된 에러로 resolve된다. */
231
+ readonly terminated: Promise<SerializedIframeCallError | null>;
232
+ /**
233
+ * iframe에 등록된 command를 호출한다.
234
+ * ready 이전 호출은 readyPolicy에 따라 queue되거나 즉시 거부된다.
235
+ * timeout/transfer는 호출별 options로 지정한다.
236
+ */
237
+ call<K extends keyof TCommands & string>(cmd: K, args: CommandArgs<TCommands[K]>, options?: IframeCallCallOptions): Promise<CommandResult<TCommands[K]>>;
238
+ /**
239
+ * iframe이 host로 보낸 notification을 event 이름과 payload로 구독한다.
240
+ * payload 타입은 `TNotificationsFromIframe`로 추론된다.
241
+ */
242
+ onNotificationFromIframe<K extends keyof TNotificationsFromIframe & string>(event: K, handler: NotifyHandler<TNotificationsFromIframe[K]>): () => void;
243
+ /**
244
+ * controller를 종료하고 transport listener를 정리한다.
245
+ * iframe에 dispose request를 한 번 시도하지만 실패해도 내부 상태는 항상 정리된다.
246
+ */
247
+ dispose(reason?: string): Promise<void>;
248
+ /**
249
+ * 개발/디버그 패널이 host 측 통신 흐름을 관찰할 수 있도록 raw event를 흘려준다.
250
+ * raw payload를 그대로 전달하므로 production logging에는 사용하지 않는다.
251
+ */
252
+ readonly debug: {
253
+ subscribe(handler: (event: HostDebugEvent) => void): () => void;
254
+ };
255
+ };
256
+ /**
257
+ * notification map 명시 여부를 판별하는 conditional helper.
258
+ * 명시된 키 union이면 false, default `Record<string, unknown>`이나 `unknown` 같은 wildcard면 true가 된다.
259
+ *
260
+ * `unknown`인 경우 `keyof = never`라 별도로 wildcard로 취급한다.
261
+ */
262
+ type IsWildcardNotificationMap<TNotificationsToHost> = unknown extends TNotificationsToHost ? true : string extends keyof TNotificationsToHost & string ? true : false;
263
+ /**
264
+ * iframe runner public surface.
265
+ * 두 generic 모두 default를 두어 단일 generic 호출처가 typecheck를 유지한다.
266
+ */
267
+ type IframeCallRunnerHandle<TCommands = Record<string, CommandHandler>, TNotificationsToHost = Record<string, unknown>> = {
268
+ /** runner가 생성한 Commands 인스턴스. class API 사용 시 host와 같은 reference를 노출한다. */
269
+ readonly commands: TCommands;
270
+ /** Commands constructor에 주입된 helper와 같은 reference. debug 구독에도 사용한다. */
271
+ readonly iframeHelper: IframeHelper<TNotificationsToHost>;
272
+ /** iframeHelper.sendNotificationToHost와 동일한 동작을 runner handle에서 노출한다. */
273
+ sendNotificationToHost: IsWildcardNotificationMap<TNotificationsToHost> extends true ? (event: string, payload: unknown) => void : <K extends DomainNotificationKey<TNotificationsToHost>>(event: K, payload: TNotificationsToHost[K]) => void;
274
+ /** transport lifecycle ready 신호. payload는 `{ protocolVersion: 1 }`로 고정된다. */
275
+ sendReadyToHost(): void;
276
+ terminated(reason: string, error?: SerializedIframeCallError): void;
277
+ /** local transport subscription을 즉시 정리하고 이후 inbound/outbound를 모두 no-op으로 만든다. */
278
+ dispose(reason?: string): void;
279
+ };
280
+
281
+ type ErrorOptions = {
282
+ readonly command?: string;
283
+ readonly details?: unknown;
284
+ readonly cause?: SerializedIframeCallError;
285
+ };
286
+ /**
287
+ * 직렬화된 iframecall 에러 객체를 생성한다.
288
+ * `command`/`details`/`cause`가 명시적으로 전달되지 않으면 결과 객체에서도 키를 누락시켜
289
+ * postMessage 직렬화 결과가 호출자 의도와 어긋나지 않게 한다.
290
+ */
291
+ declare function createIframeCallError(code: string, message: string, options?: ErrorOptions): SerializedIframeCallError;
292
+ /**
293
+ * 임의의 throw 값을 직렬화된 iframecall 에러로 정규화한다.
294
+ * - 이미 직렬화된 형태면 `command` 정보가 비어 있을 때만 보강한다.
295
+ * - 실제 Error 인스턴스는 message만 살리고 name은 details에 보존한다.
296
+ * - 그 외 값은 문자열화한 결과를 message로, 원본을 details로 남긴다.
297
+ */
298
+ declare function serializeIframeCallError(value: unknown, command?: string): SerializedIframeCallError;
299
+ /**
300
+ * 임의의 값이 이미 직렬화된 iframecall 에러 형태인지 검사한다.
301
+ * `code`와 `message`가 모두 string이어야 한다.
302
+ */
303
+ declare function isSerializedIframeCallError(value: unknown): value is SerializedIframeCallError;
304
+
305
+ /** 파싱 결과를 type tag로 구분한 discriminated union. 알 수 없는 형태는 null로 반환한다. */
306
+ type ParsedIframeCallMessage = {
307
+ readonly type: "request";
308
+ readonly message: IframeCallRequest;
309
+ } | {
310
+ readonly type: "response";
311
+ readonly message: IframeCallResponse;
312
+ } | {
313
+ readonly type: "notify";
314
+ readonly message: IframeCallNotify;
315
+ };
316
+ /** host -> iframe 방향 command 요청 envelope을 만든다. */
317
+ declare function createIframeCallRequest(id: string, cmd: string, args: readonly unknown[]): IframeCallRequest;
318
+ /** iframe -> host 방향 command 성공 응답 envelope을 만든다. */
319
+ declare function createIframeCallSuccessResponse(id: string, value: unknown): IframeCallResponse;
320
+ /** iframe -> host 방향 command 실패 응답 envelope을 만든다. */
321
+ declare function createIframeCallErrorResponse(id: string, error: SerializedIframeCallError): IframeCallResponse;
322
+ /** iframe -> host 방향 notify envelope을 만든다. id 없이 event 이름과 payload만 가진다. */
323
+ declare function createIframeCallNotify(event: string, payload: unknown): IframeCallNotify;
324
+ /**
325
+ * postMessage로 받은 임의의 값을 iframecall envelope으로 파싱한다.
326
+ * `protocol`/`version`이 어긋나거나 envelope 형태가 어떤 분기와도 일치하지 않으면 null을 돌려준다.
327
+ * 호출 측은 origin/source 검증 후 이 함수의 결과를 신뢰해 dispatch한다.
328
+ */
329
+ declare function parseIframeCallMessage(value: unknown): ParsedIframeCallMessage | null;
330
+
331
+ export { type CommandsConstructor as A, type IframeCallRunnerClassOptions as B, type CommandMap as C, type IframeHelper as D, type DomainNotificationKey as E, createParentWindowTransport as F, type HostDebugEvent as H, type IframeCallControllerOptions as I, type NotifyHandler as N, type ParsedIframeCallMessage as P, type ReadyPolicy as R, type SerializedIframeCallError as S, type IframeCallController as a, type CommandArgs as b, type CommandHandler as c, type CommandResult as d, type CommandRunner as e, type IframeCallCallOptions as f, type IframeCallLogger as g, type IframeCallNotify as h, type IframeCallRequest as i, type IframeCallResponse as j, type IframeCallTransferable as k, type IframeCallTransport as l, type IframeCallTransportEvent as m, type ReservedNotificationName as n, createIframeCallError as o, createIframeCallErrorResponse as p, createIframeCallNotify as q, createIframeCallRequest as r, createIframeCallSuccessResponse as s, createIframeWindowTransport as t, isSerializedIframeCallError as u, parseIframeCallMessage as v, serializeIframeCallError as w, type IframeDebugEvent as x, type IframeCallRunnerOptions as y, type IframeCallRunnerHandle as z };