@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/host.js ADDED
@@ -0,0 +1,618 @@
1
+ import {
2
+ createIframeCallError,
3
+ createIframeCallErrorResponse,
4
+ createIframeCallNotify,
5
+ createIframeCallRequest,
6
+ createIframeCallSuccessResponse,
7
+ createIframeWindowTransport,
8
+ isSerializedIframeCallError,
9
+ parseIframeCallMessage,
10
+ serializeIframeCallError
11
+ } from "./chunk-UYZXYOI6.js";
12
+
13
+ // src/host/consoleDebugLogger.ts
14
+ function consoleDebugLogger(options = {}) {
15
+ const prefix = options.prefix ?? "[iframecall:host]";
16
+ const head = prefix.length > 0 ? `${prefix} ` : "";
17
+ return (event) => {
18
+ switch (event.type) {
19
+ case "commandSentToIframe":
20
+ console.debug(`${head}${event.type} ${event.command}`, event.args);
21
+ return;
22
+ case "commandResultReceivedFromIframe":
23
+ console.debug(`${head}${event.type} ${event.command}`, event.value);
24
+ return;
25
+ case "commandErrorReceivedFromIframe":
26
+ console.debug(`${head}${event.type} ${event.command}`, event.error);
27
+ return;
28
+ case "notificationReceivedFromIframe":
29
+ console.debug(`${head}${event.type} ${event.event}`, event.payload);
30
+ return;
31
+ case "readyReceived":
32
+ console.debug(`${head}${event.type}`, event.payload);
33
+ return;
34
+ case "terminatedReceived":
35
+ console.debug(`${head}${event.type} ${event.reason}`, event.error);
36
+ return;
37
+ }
38
+ };
39
+ }
40
+
41
+ // src/host/controllerLifecycle.ts
42
+ function createControllerLifecycle(options) {
43
+ const { readyTimeoutMs, onTerminate } = options;
44
+ let readyFlag = false;
45
+ let terminatedError = null;
46
+ let cleanupDone = false;
47
+ let onCleanup = null;
48
+ let resolveReady;
49
+ let rejectReady;
50
+ const ready = new Promise((resolve, reject) => {
51
+ resolveReady = resolve;
52
+ rejectReady = reject;
53
+ });
54
+ ready.catch(() => {
55
+ });
56
+ let resolveTerminated;
57
+ const terminated = new Promise(
58
+ (resolve) => {
59
+ resolveTerminated = resolve;
60
+ }
61
+ );
62
+ const readyTimeoutId = readyTimeoutMs === 0 || readyTimeoutMs === Number.POSITIVE_INFINITY ? null : setTimeout(() => {
63
+ terminate(
64
+ createIframeCallError("timeout", "Iframe ready timed out.", {
65
+ details: { timeoutMs: readyTimeoutMs }
66
+ })
67
+ );
68
+ }, readyTimeoutMs);
69
+ function clearReadyTimeout() {
70
+ if (readyTimeoutId !== null) {
71
+ clearTimeout(readyTimeoutId);
72
+ }
73
+ }
74
+ function markReady() {
75
+ if (terminatedError !== null || readyFlag) {
76
+ return;
77
+ }
78
+ readyFlag = true;
79
+ clearReadyTimeout();
80
+ resolveReady();
81
+ }
82
+ function terminate(error) {
83
+ if (terminatedError !== null) {
84
+ return;
85
+ }
86
+ terminatedError = error;
87
+ clearReadyTimeout();
88
+ rejectReady(error);
89
+ resolveTerminated(error);
90
+ onTerminate(error);
91
+ cleanup();
92
+ }
93
+ function cleanup() {
94
+ if (cleanupDone) {
95
+ return;
96
+ }
97
+ cleanupDone = true;
98
+ onCleanup?.();
99
+ }
100
+ return {
101
+ ready,
102
+ terminated,
103
+ isReady: () => readyFlag,
104
+ isTerminated: () => terminatedError !== null,
105
+ getTerminatedError: () => terminatedError,
106
+ markReady,
107
+ terminate,
108
+ setOnCleanup(fn) {
109
+ onCleanup = fn;
110
+ },
111
+ cleanup
112
+ };
113
+ }
114
+
115
+ // src/host/notifyHandlerRegistry.ts
116
+ function createNotifyHandlerRegistry() {
117
+ const handlers = /* @__PURE__ */ new Map();
118
+ return {
119
+ register(event, handler) {
120
+ const set = handlers.get(event) ?? /* @__PURE__ */ new Set();
121
+ set.add(handler);
122
+ handlers.set(event, set);
123
+ return () => {
124
+ set.delete(handler);
125
+ if (set.size === 0) {
126
+ handlers.delete(event);
127
+ }
128
+ };
129
+ },
130
+ dispatch(event, payload) {
131
+ const set = handlers.get(event);
132
+ if (set === void 0) {
133
+ return;
134
+ }
135
+ for (const handler of set) {
136
+ handler(payload);
137
+ }
138
+ },
139
+ clear() {
140
+ handlers.clear();
141
+ }
142
+ };
143
+ }
144
+
145
+ // src/host/pendingCallRegistry.ts
146
+ function createPendingCallRegistry(transport, targetOrigin) {
147
+ const pending = /* @__PURE__ */ new Map();
148
+ function clearCallTimeout(call) {
149
+ if (call.timeoutId !== null) {
150
+ clearTimeout(call.timeoutId);
151
+ }
152
+ }
153
+ return {
154
+ add(id, call) {
155
+ pending.set(id, call);
156
+ },
157
+ delete(id) {
158
+ pending.delete(id);
159
+ },
160
+ settle(id, response) {
161
+ const call = pending.get(id);
162
+ if (call === void 0) {
163
+ return;
164
+ }
165
+ pending.delete(id);
166
+ clearCallTimeout(call);
167
+ if (response.ok) {
168
+ call.resolve(response.value);
169
+ return;
170
+ }
171
+ call.reject(response.error);
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);
217
+ }
218
+ }
219
+ return {
220
+ size() {
221
+ return queued.size;
222
+ },
223
+ add(id, call) {
224
+ queued.set(id, call);
225
+ },
226
+ delete(id) {
227
+ queued.delete(id);
228
+ },
229
+ flush(forward) {
230
+ for (const [id, call] of queued) {
231
+ queued.delete(id);
232
+ forward(id, call);
233
+ }
234
+ },
235
+ rejectAll(buildError) {
236
+ for (const [, call] of queued) {
237
+ clearCallTimeout(call);
238
+ call.reject(buildError(call.command));
239
+ }
240
+ queued.clear();
241
+ }
242
+ };
243
+ }
244
+
245
+ // src/host/controller.ts
246
+ function createIframeCallController(options) {
247
+ const targetOrigin = requireTargetOrigin(options.targetOrigin);
248
+ const allowedOrigins = new Set(options.allowedOrigins ?? [targetOrigin]);
249
+ const transport = options.transport ?? createIframeWindowTransport(options.iframe);
250
+ const generateId = options.generateId ?? crypto.randomUUID.bind(crypto);
251
+ const defaultTimeoutMs = options.defaultTimeoutMs ?? 3e4;
252
+ const readyTimeoutMs = options.readyTimeoutMs ?? defaultTimeoutMs;
253
+ const readyPolicy = options.readyPolicy ?? "queue";
254
+ const readyQueueLimit = options.readyQueueLimit ?? Number.POSITIVE_INFINITY;
255
+ const pending = createPendingCallRegistry(transport, targetOrigin);
256
+ const queue = createReadyQueue();
257
+ const notifyRegistry = createNotifyHandlerRegistry();
258
+ const debugSubscribers = /* @__PURE__ */ new Set();
259
+ function emitDebug(event) {
260
+ for (const handler of debugSubscribers) {
261
+ try {
262
+ handler(event);
263
+ } catch (error) {
264
+ options.logger?.warn("iframecall debug subscriber threw.", error);
265
+ }
266
+ }
267
+ }
268
+ const lifecycle = createControllerLifecycle({
269
+ readyTimeoutMs,
270
+ onTerminate(error) {
271
+ const buildLifecycleError = (command) => createCallLifecycleError(error, command);
272
+ pending.rejectAll(buildLifecycleError);
273
+ queue.rejectAll(buildLifecycleError);
274
+ }
275
+ });
276
+ const unsubscribeTransport = transport.subscribe(
277
+ createTransportRouter({
278
+ lifecycle,
279
+ allowedOrigins,
280
+ transport,
281
+ pending,
282
+ queue,
283
+ notifyRegistry,
284
+ logger: options.logger,
285
+ emitDebug
286
+ })
287
+ );
288
+ lifecycle.setOnCleanup(() => {
289
+ unsubscribeTransport();
290
+ notifyRegistry.clear();
291
+ });
292
+ const controller = {
293
+ ready: lifecycle.ready,
294
+ terminated: lifecycle.terminated,
295
+ call(cmd, args, callOptions) {
296
+ const terminatedError = lifecycle.getTerminatedError();
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
+ });
354
+ },
355
+ onNotificationFromIframe(event, handler) {
356
+ return notifyRegistry.register(
357
+ event,
358
+ handler
359
+ );
360
+ },
361
+ debug: {
362
+ subscribe(handler) {
363
+ debugSubscribers.add(handler);
364
+ return () => {
365
+ debugSubscribers.delete(handler);
366
+ };
367
+ }
368
+ },
369
+ async dispose(reason = "host_requested") {
370
+ if (lifecycle.isTerminated()) {
371
+ lifecycle.cleanup();
372
+ return;
373
+ }
374
+ const lifecycleError = createIframeCallError(
375
+ "terminated",
376
+ "Controller disposed.",
377
+ {
378
+ details: { reason }
379
+ }
380
+ );
381
+ try {
382
+ transport.post(
383
+ createIframeCallRequest(generateId(), "host:dispose", [{ reason }]),
384
+ targetOrigin
385
+ );
386
+ } catch (error) {
387
+ options.logger?.warn("iframecall dispose message failed.", error);
388
+ } finally {
389
+ lifecycle.terminate(lifecycleError);
390
+ lifecycle.cleanup();
391
+ }
392
+ }
393
+ };
394
+ return controller;
395
+ }
396
+ function createTransportRouter(deps) {
397
+ const {
398
+ lifecycle,
399
+ allowedOrigins,
400
+ transport,
401
+ pending,
402
+ queue,
403
+ notifyRegistry,
404
+ logger,
405
+ emitDebug
406
+ } = deps;
407
+ return (event) => {
408
+ if (lifecycle.isTerminated()) return;
409
+ if (!allowedOrigins.has(event.origin)) return;
410
+ if (transport.expectedSource !== void 0 && event.source !== transport.expectedSource) {
411
+ return;
412
+ }
413
+ const parsed = parseIframeCallMessage(event.data);
414
+ if (parsed?.type === "response") {
415
+ const responseMessage = parsed.message;
416
+ const command = pending.getCommand(responseMessage.id);
417
+ pending.settle(responseMessage.id, responseMessage);
418
+ if (command !== void 0) {
419
+ if (responseMessage.ok) {
420
+ emitDebug({
421
+ type: "commandResultReceivedFromIframe",
422
+ command,
423
+ value: responseMessage.value
424
+ });
425
+ } else {
426
+ emitDebug({
427
+ type: "commandErrorReceivedFromIframe",
428
+ command,
429
+ error: responseMessage.error
430
+ });
431
+ }
432
+ }
433
+ return;
434
+ }
435
+ if (parsed?.type !== "notify") return;
436
+ const { event: notifyEvent, payload } = parsed.message;
437
+ if (notifyEvent === "ready") {
438
+ handleReadyNotify(payload, lifecycle, queue, pending, logger, emitDebug);
439
+ return;
440
+ }
441
+ if (notifyEvent === "terminated") {
442
+ const terminatedPayload = isRecord(payload) ? payload : {};
443
+ const reason = typeof terminatedPayload.reason === "string" ? terminatedPayload.reason : "unknown";
444
+ const cause = getTerminatedCause(payload);
445
+ emitDebug({
446
+ type: "terminatedReceived",
447
+ reason,
448
+ error: cause ?? null
449
+ });
450
+ lifecycle.terminate(
451
+ createIframeCallError("terminated", "Iframe terminated.", {
452
+ cause,
453
+ details: payload
454
+ })
455
+ );
456
+ return;
457
+ }
458
+ notifyRegistry.dispatch(notifyEvent, payload);
459
+ emitDebug({
460
+ type: "notificationReceivedFromIframe",
461
+ event: notifyEvent,
462
+ payload
463
+ });
464
+ };
465
+ }
466
+ function handleReadyNotify(payload, lifecycle, queue, pending, logger, emitDebug) {
467
+ if (lifecycle.isReady()) {
468
+ logger?.warn("iframecall duplicate ready ignored.", payload);
469
+ return;
470
+ }
471
+ if (!isSupportedReadyPayload(payload)) {
472
+ lifecycle.terminate(
473
+ createIframeCallError(
474
+ "version_mismatch",
475
+ "Unsupported iframecall protocol version.",
476
+ { details: payload }
477
+ )
478
+ );
479
+ return;
480
+ }
481
+ emitDebug({ type: "readyReceived", payload });
482
+ lifecycle.markReady();
483
+ queue.flush((id, call) => {
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
+ }
493
+ function createCallLifecycleError(error, command) {
494
+ if (error.code !== "timeout" || error.command !== void 0) {
495
+ return error;
496
+ }
497
+ return {
498
+ ...error,
499
+ command
500
+ };
501
+ }
502
+ function getTerminatedCause(payload) {
503
+ if (!isRecord(payload) || !isSerializedIframeCallError(payload.error)) {
504
+ return void 0;
505
+ }
506
+ return payload.error;
507
+ }
508
+ function isSupportedReadyPayload(payload) {
509
+ return isRecord(payload) && payload.protocolVersion === 1;
510
+ }
511
+ function requireTargetOrigin(targetOrigin) {
512
+ if (targetOrigin.length === 0 || targetOrigin === "*" || targetOrigin === "null") {
513
+ throw createIframeCallError(
514
+ "invalid_origin",
515
+ "targetOrigin must be explicit."
516
+ );
517
+ }
518
+ return targetOrigin;
519
+ }
520
+ function isRecord(value) {
521
+ return typeof value === "object" && value !== null;
522
+ }
523
+
524
+ // src/host/useIframeCallController.tsx
525
+ import { useCallback, useEffect, useRef, useState } from "react";
526
+ function useIframeCallController(options) {
527
+ const optionsRef = useRef(options);
528
+ optionsRef.current = options;
529
+ const iframeElRef = useRef(null);
530
+ const [iframeAttached, setIframeAttached] = useState(false);
531
+ const [controller, setController] = useState(null);
532
+ const [status, setStatus] = useState("pending");
533
+ const [terminationError, setTerminationError] = useState(null);
534
+ const [readyError, setReadyError] = useState(null);
535
+ const iframeRef = useCallback((node) => {
536
+ iframeElRef.current = node;
537
+ setIframeAttached(node !== null);
538
+ }, []);
539
+ useEffect(() => {
540
+ if (!iframeAttached) return;
541
+ const iframeEl = iframeElRef.current;
542
+ if (iframeEl === null) return;
543
+ const opts = optionsRef.current;
544
+ const next = createIframeCallController({
545
+ iframe: iframeEl,
546
+ targetOrigin: opts.targetOrigin,
547
+ allowedOrigins: opts.allowedOrigins,
548
+ defaultTimeoutMs: opts.defaultTimeoutMs,
549
+ generateId: opts.generateId,
550
+ logger: opts.logger,
551
+ readyPolicy: opts.readyPolicy,
552
+ readyQueueLimit: opts.readyQueueLimit,
553
+ readyTimeoutMs: opts.readyTimeoutMs,
554
+ transport: opts.transport
555
+ });
556
+ let cancelled = false;
557
+ setController(
558
+ next
559
+ );
560
+ setStatus("pending");
561
+ setTerminationError(null);
562
+ setReadyError(null);
563
+ next.ready.then(
564
+ () => {
565
+ if (cancelled) return;
566
+ setStatus("ready");
567
+ },
568
+ (error) => {
569
+ if (cancelled) return;
570
+ setReadyError(error);
571
+ setStatus((prev) => prev === "terminated" ? prev : "failed");
572
+ }
573
+ );
574
+ next.terminated.then((reason) => {
575
+ if (cancelled) return;
576
+ setTerminationError(reason);
577
+ setStatus("terminated");
578
+ });
579
+ let unsubscribeDebug = null;
580
+ const debugLog = opts.debugLog;
581
+ if (debugLog) {
582
+ const prefix = typeof debugLog === "object" && debugLog !== null ? debugLog.prefix : void 0;
583
+ const handler = consoleDebugLogger(
584
+ prefix !== void 0 ? { prefix } : void 0
585
+ );
586
+ unsubscribeDebug = next.debug.subscribe(handler);
587
+ }
588
+ return () => {
589
+ cancelled = true;
590
+ if (unsubscribeDebug !== null) {
591
+ unsubscribeDebug();
592
+ }
593
+ void next.dispose("host-unmount");
594
+ setController(null);
595
+ };
596
+ }, [iframeAttached]);
597
+ return {
598
+ iframeRef,
599
+ controller,
600
+ status,
601
+ terminationError,
602
+ readyError
603
+ };
604
+ }
605
+ export {
606
+ consoleDebugLogger,
607
+ createIframeCallController,
608
+ createIframeCallError,
609
+ createIframeCallErrorResponse,
610
+ createIframeCallNotify,
611
+ createIframeCallRequest,
612
+ createIframeCallSuccessResponse,
613
+ createIframeWindowTransport,
614
+ isSerializedIframeCallError,
615
+ parseIframeCallMessage,
616
+ serializeIframeCallError,
617
+ useIframeCallController
618
+ };
@@ -0,0 +1,67 @@
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-Cg4n93bf.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-Cg4n93bf.js';
3
+
4
+ /** consoleDebugLogger 옵션. prefix를 명시하지 않으면 [iframecall:iframe]가 기본값이다. */
5
+ type ConsoleDebugLoggerOptions = {
6
+ readonly prefix?: string;
7
+ };
8
+ /**
9
+ * iframe runner의 iframeHelper.debug.subscribe에 그대로 넘길 수 있는 console.debug 출력 함수.
10
+ * event type별로 핵심 식별자(command/event)를 첫 인자에 포함하고,
11
+ * raw payload(args/value/error/payload)를 두 번째 인자로 전달한다.
12
+ */
13
+ declare function consoleDebugLogger(options?: ConsoleDebugLoggerOptions): (event: IframeDebugEvent) => void;
14
+
15
+ /**
16
+ * iframecall iframe runner를 생성한다.
17
+ * `{ Commands }` class 옵션을 받아 dispatch lookup을 만들고,
18
+ * 도메인 코드가 host로 notify를 보낼 수 있는 helper를 함께 노출한다.
19
+ * 두 번째 generic은 host로 보낼 notify event/payload 타입 추론에 사용한다.
20
+ */
21
+ declare function createIframeCallRunner<TCommands extends CommandMap<TCommands>, TNotificationsToHost = Record<string, unknown>>(options: IframeCallRunnerOptions<TCommands, TNotificationsToHost>): IframeCallRunnerHandle<TCommands, TNotificationsToHost>;
22
+
23
+ /** debugLog 옵션. true면 자동 구독, prefix override 가능. false/undefined면 미구독. */
24
+ type UseIframeCallRunnerDebugLog = boolean | {
25
+ readonly prefix?: string;
26
+ };
27
+ /**
28
+ * useIframeCallRunner에 전달하는 옵션.
29
+ * IframeCallRunnerClassOptions의 transport, logger, onHostDispose도 그대로 전달할 수 있다.
30
+ */
31
+ type UseIframeCallRunnerOptions<TCommands, TNotificationsToHost = Record<string, unknown>> = {
32
+ /** postMessage targetOrigin. 빈 문자열이나 "*"은 runner 내부에서 거부된다. */
33
+ readonly targetOrigin: string;
34
+ /** 허용할 origin 목록. 생략하면 targetOrigin 하나만 허용한다. */
35
+ readonly allowedOrigins?: readonly string[];
36
+ /** runner가 new해서 iframeHelper를 주입할 Commands class. */
37
+ readonly Commands: CommandsConstructor<TCommands, TNotificationsToHost>;
38
+ /** true 또는 { prefix }이면 mount 시 consoleDebugLogger를 자동 구독한다. */
39
+ readonly debugLog?: UseIframeCallRunnerDebugLog;
40
+ } & Pick<IframeCallRunnerClassOptions<TCommands, TNotificationsToHost>, "logger" | "onHostDispose" | "transport">;
41
+ /**
42
+ * useIframeCallRunner가 반환하는 handle.
43
+ * mount 전에는 모든 값이 undefined / false이며, mount 후 runner가 생성되면 채워진다.
44
+ */
45
+ type UseIframeCallRunnerResult<TCommands, TNotificationsToHost = Record<string, unknown>> = {
46
+ /** runner가 생성한 Commands 인스턴스. mount 전 undefined. */
47
+ readonly commands: TCommands | undefined;
48
+ /** Commands constructor에 주입된 iframeHelper. mount 전 undefined. */
49
+ readonly iframeHelper: IframeHelper<TNotificationsToHost> | undefined;
50
+ /** runner handle 전체. mount 전 undefined. */
51
+ readonly runner: IframeCallRunnerHandle<TCommands, TNotificationsToHost> | undefined;
52
+ /** runner가 활성화되어 있으면 true. mount 전 false. */
53
+ readonly isActive: boolean;
54
+ };
55
+ /**
56
+ * IframeCallRunner를 React lifecycle에 묶는 hook.
57
+ * mount 시 runner를 한 번만 생성하고, unmount 시 dispose한다.
58
+ * re-render로 인한 runner 재생성을 방지하기 위해 handle을 ref에 보관한다.
59
+ * unmount cleanup은 transport/runner dispose만 수행하고 isActive 전환은 하지 않는다.
60
+ * StrictMode의 mount→unmount→re-mount 사이클에서도 inactive 상태가 외부에 노출되지 않도록 보장한다.
61
+ *
62
+ * TCommands는 Commands class가 구현하는 command interface여야 한다.
63
+ * class 자체가 아니라 command method만 포함하는 타입을 전달해야 CommandMap constraint를 만족한다.
64
+ */
65
+ declare function useIframeCallRunner<TCommands extends CommandMap<TCommands>, TNotificationsToHost = Record<string, unknown>>(options: UseIframeCallRunnerOptions<TCommands, TNotificationsToHost>): UseIframeCallRunnerResult<TCommands, TNotificationsToHost>;
66
+
67
+ export { CommandMap, CommandsConstructor, IframeCallRunnerClassOptions, IframeCallRunnerHandle, IframeCallRunnerOptions, IframeDebugEvent, IframeHelper, type UseIframeCallRunnerDebugLog, type UseIframeCallRunnerOptions, type UseIframeCallRunnerResult, consoleDebugLogger, createIframeCallRunner, useIframeCallRunner };