@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/LICENSE +21 -0
- package/README.md +261 -0
- package/dist/chunk-UYZXYOI6.js +175 -0
- package/dist/host.d.ts +52 -0
- package/dist/host.js +618 -0
- package/dist/iframe.d.ts +67 -0
- package/dist/iframe.js +299 -0
- package/dist/messages-Cg4n93bf.d.ts +331 -0
- package/package.json +66 -0
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
|
+
};
|
package/dist/iframe.d.ts
ADDED
|
@@ -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 };
|