@blocksdiy/react-common 1.37.0 → 1.39.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/components/new-agent-chat.d.ts +19 -0
- package/dist/components/new-agent-chat.d.ts.map +1 -1
- package/dist/components/new-agent-chat.js +115 -12
- package/dist/iframe-communication/bridge-provider.d.ts.map +1 -1
- package/dist/iframe-communication/bridge-provider.js +27 -18
- package/dist/iframe-communication/iframe-bridge-provider.d.ts +7 -4
- package/dist/iframe-communication/iframe-bridge-provider.d.ts.map +1 -1
- package/dist/iframe-communication/iframe-bridge-provider.js +24 -51
- package/dist/iframe-communication/index.d.ts +1 -1
- package/dist/iframe-communication/index.d.ts.map +1 -1
- package/dist/iframe-communication/index.js +3 -1
- package/dist/iframe-communication/live-methods.d.ts +19 -0
- package/dist/iframe-communication/live-methods.d.ts.map +1 -0
- package/dist/iframe-communication/live-methods.js +28 -0
- package/dist/iframe-communication/platform-bridge-provider.d.ts +3 -2
- package/dist/iframe-communication/platform-bridge-provider.d.ts.map +1 -1
- package/dist/iframe-communication/platform-bridge-provider.js +21 -54
- package/dist/iframe-communication/promise-with-resolvers-polyfill.d.ts +2 -0
- package/dist/iframe-communication/promise-with-resolvers-polyfill.d.ts.map +1 -0
- package/dist/iframe-communication/promise-with-resolvers-polyfill.js +19 -0
- package/dist/iframe-communication/use-bridge-connection.d.ts +34 -0
- package/dist/iframe-communication/use-bridge-connection.d.ts.map +1 -0
- package/dist/iframe-communication/use-bridge-connection.js +128 -0
- package/package.json +5 -3
|
@@ -61,6 +61,14 @@ export interface AgentChatContextValue {
|
|
|
61
61
|
renderToolCall: ReturnType<typeof useRenderToolCall>;
|
|
62
62
|
sendMessage: (message: Omit<UserMessage, "id" | "role">) => Promise<void>;
|
|
63
63
|
sendFromInputs: () => Promise<void>;
|
|
64
|
+
/** Resumes a run parked on a server-side interrupt. Rejects if the resume fails. */
|
|
65
|
+
/** Resolves false when a run was already in flight, so nothing was sent. */
|
|
66
|
+
resolveInterrupt: (resume: unknown) => Promise<boolean>;
|
|
67
|
+
/** Answers one parked card; resumes once every card of the turn is answered. */
|
|
68
|
+
answerInterrupt: (answer: {
|
|
69
|
+
toolCallId: string;
|
|
70
|
+
answer: unknown;
|
|
71
|
+
}) => Promise<void>;
|
|
64
72
|
stopGeneration: () => void;
|
|
65
73
|
addAttachments: (attachments: Attachment[]) => void;
|
|
66
74
|
removeAttachment: (url: string) => void;
|
|
@@ -95,6 +103,17 @@ export interface AgentChatRootProps {
|
|
|
95
103
|
* `agent-self-modify` flag) — never in the published app.
|
|
96
104
|
*/
|
|
97
105
|
selfEditEnabled?: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Set by a chat surface that carries renderers for the server-declared pauses
|
|
108
|
+
* (`ask_user` and `ask-component-…`). The backend offers those tools ONLY when
|
|
109
|
+
* this is true, and then strips their browser-side predecessors.
|
|
110
|
+
*
|
|
111
|
+
* It must originate in the surface's own component, not here: this package ships
|
|
112
|
+
* with the platform and is identical everywhere, while each app carries its own
|
|
113
|
+
* frozen copy of the chat component — and that copy is the only thing that differs
|
|
114
|
+
* between an app that can draw these pauses and one that cannot.
|
|
115
|
+
*/
|
|
116
|
+
supportsServerInterrupt?: boolean;
|
|
98
117
|
}
|
|
99
118
|
export declare const AgentChatRoot: ({ ...props }: AgentChatRootProps) => import("react").JSX.Element | null;
|
|
100
119
|
export interface AgentChatMessageProps {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAiB,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EAEL,SAAS,EACT,cAAc,EAQf,MAAM,OAAO,CAAC;AAGf,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAEzE,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACtF,OAAO,EACL,aAAa,EACb,oBAAoB,EACpB,iBAAiB,EACjB,eAAe,EACf,cAAc,GACf,MAAM,2BAA2B,CAAC;AAOnC,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,eAAO,MAAM,6BAA6B,GAAI,MAAM,MAAM,WAEzD,CAAC;AAEF,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7D,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"new-agent-chat.d.ts","sourceRoot":"","sources":["../../src/components/new-agent-chat.tsx"],"names":[],"mappings":"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"}
|
|
@@ -26,9 +26,7 @@ const generateId = (length = 10) => {
|
|
|
26
26
|
.toString(36)
|
|
27
27
|
.substring(2, 2 + length);
|
|
28
28
|
};
|
|
29
|
-
// Assistant tool calls with no matching ToolMessage
|
|
30
|
-
// frontend tools (e.g. user-choice) reach this state; backend tools resolve in
|
|
31
|
-
// the same run. Used to auto-skip dangling answer UIs when the user types past.
|
|
29
|
+
// Assistant tool calls with no matching ToolMessage.
|
|
32
30
|
const _collectPendingToolCalls = (messages) => {
|
|
33
31
|
const toolMessageIds = new Set();
|
|
34
32
|
for (const m of messages) {
|
|
@@ -49,6 +47,35 @@ const _collectPendingToolCalls = (messages) => {
|
|
|
49
47
|
}
|
|
50
48
|
return pending;
|
|
51
49
|
};
|
|
50
|
+
// Message order IS the order of the interrupt's `action_requests`, so decisions
|
|
51
|
+
// pair with them by index and need no ids. Matched by name rather than by "has no
|
|
52
|
+
// local handler", so a backend tool merely mid-flight is never taken for a pause.
|
|
53
|
+
const _SERVER_PAUSE_TOOL_NAME = "ask_user";
|
|
54
|
+
const _SERVER_PAUSE_TOOL_PREFIX = "ask-component-";
|
|
55
|
+
const _isServerPause = (toolCall) => toolCall.function.name === _SERVER_PAUSE_TOOL_NAME || toolCall.function.name.startsWith(_SERVER_PAUSE_TOOL_PREFIX);
|
|
56
|
+
// ONE assistant message's pauses, never the transcript's: the server batches a
|
|
57
|
+
// turn's pauses into a single interrupt requiring exactly one decision each, and a
|
|
58
|
+
// transcript can also hold pauses from a run that died before parking. Counting
|
|
59
|
+
// those raises the count check — permanently, since the checkpoint never advances.
|
|
60
|
+
const _collectServerPauses = (messages) => {
|
|
61
|
+
const answered = new Set();
|
|
62
|
+
for (const m of messages) {
|
|
63
|
+
if (m.role === "tool" && "toolCallId" in m && typeof m.toolCallId === "string") {
|
|
64
|
+
answered.add(m.toolCallId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
68
|
+
const m = messages[i];
|
|
69
|
+
if (m.role !== "assistant" || !("toolCalls" in m) || !m.toolCalls) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const pauses = m.toolCalls.filter((toolCall) => !answered.has(toolCall.id) && _isServerPause(toolCall));
|
|
73
|
+
if (pauses.length > 0) {
|
|
74
|
+
return pauses;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
};
|
|
52
79
|
// CopilotKit 1.60.x: `connectAgent()` awaits every replayed HITL tool call's
|
|
53
80
|
// handler before resolving `isAvailable`, and a `useHumanInTheLoop` handler
|
|
54
81
|
// only resolves on `respond()` — so a transcript replayed with an unanswered
|
|
@@ -281,6 +308,7 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
281
308
|
});
|
|
282
309
|
return groups;
|
|
283
310
|
}, [copilotMessages]);
|
|
311
|
+
const interruptAnswersRef = useRef(new Map());
|
|
284
312
|
// Mirror `currentThreadId` onto the agent instance — the `<CopilotKit>`
|
|
285
313
|
// prop only feeds React context, but `agent.threadId` is what hits the
|
|
286
314
|
// wire and the LangGraph checkpoint. `<CopilotChat>` does this for you;
|
|
@@ -292,6 +320,9 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
292
320
|
if (copilotAgent && currentThreadId) {
|
|
293
321
|
copilotAgent.threadId = currentThreadId;
|
|
294
322
|
}
|
|
323
|
+
// Carrying answers across a thread swap would pair them with another
|
|
324
|
+
// conversation's pause.
|
|
325
|
+
interruptAnswersRef.current.clear();
|
|
295
326
|
}, [copilotAgent, currentThreadId]);
|
|
296
327
|
const [prompt, setPrompt] = useState("");
|
|
297
328
|
const [attachments, setAttachments] = useState([]);
|
|
@@ -302,9 +333,9 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
302
333
|
// CopilotKit reports `isLoading` during connect/replay as well as real runs.
|
|
303
334
|
// Only expose thinking once the bootstrap connect phase has completed.
|
|
304
335
|
const isThinking = isAgentRunning && !isFetchingMessages;
|
|
305
|
-
const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart) => {
|
|
336
|
+
const runAgentWithRetry = useCallback(async (agentToRun, messageCountAtRunStart, forwardedProps) => {
|
|
306
337
|
if (agentToRun.isRunning) {
|
|
307
|
-
return;
|
|
338
|
+
return false;
|
|
308
339
|
}
|
|
309
340
|
const runAgentAndThrowError = async () => {
|
|
310
341
|
let runEventError;
|
|
@@ -317,7 +348,7 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
317
348
|
},
|
|
318
349
|
});
|
|
319
350
|
try {
|
|
320
|
-
await copilotkit.runAgent({ agent: agentToRun });
|
|
351
|
+
await copilotkit.runAgent({ agent: agentToRun, forwardedProps });
|
|
321
352
|
if (runEventError) {
|
|
322
353
|
throw runEventError;
|
|
323
354
|
}
|
|
@@ -334,18 +365,75 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
334
365
|
const status = getAgentRunErrorStatus(error);
|
|
335
366
|
if (status !== undefined && status >= 500 && status < 600 && !hasAgentStartedResponding) {
|
|
336
367
|
await runAgentAndThrowError();
|
|
337
|
-
return;
|
|
368
|
+
return true;
|
|
338
369
|
}
|
|
339
370
|
throw error;
|
|
340
371
|
}
|
|
372
|
+
return true;
|
|
341
373
|
}, [copilotkit]);
|
|
342
|
-
const sendMessage = useCallback((message) => {
|
|
374
|
+
const sendMessage = useCallback(async (message) => {
|
|
343
375
|
if (!canSendMessage || !copilotAgent || copilotAgent.isRunning) {
|
|
344
|
-
return
|
|
376
|
+
return;
|
|
345
377
|
}
|
|
346
378
|
copilotAgent.addMessage({ ...message, id: generateId(), role: "user" });
|
|
347
|
-
|
|
379
|
+
await runAgentWithRetry(copilotAgent, copilotAgent.messages.length);
|
|
348
380
|
}, [canSendMessage, copilotAgent, runAgentWithRetry]);
|
|
381
|
+
// Not sourced from `useInterrupt`: that hook only hands `resolve` to its own
|
|
382
|
+
// `render`, which it skips whenever nothing is pending — so after a reload there
|
|
383
|
+
// is no way to obtain it. `runAgentWithRetry`'s `isRunning` guard makes a
|
|
384
|
+
// double-click a no-op rather than a duplicate resume.
|
|
385
|
+
const resolveInterrupt = useCallback(async (resume) => {
|
|
386
|
+
if (!copilotAgent) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
return runAgentWithRetry(copilotAgent, copilotAgent.messages.length, { command: { resume } });
|
|
390
|
+
}, [copilotAgent, runAgentWithRetry]);
|
|
391
|
+
// Resumes only once EVERY pause of the turn has an answer: a short decisions list
|
|
392
|
+
// fails `HumanInTheLoopMiddleware`'s count check, and the checkpoint never
|
|
393
|
+
// advances, so every later answer fails identically.
|
|
394
|
+
const answerInterrupt = useCallback(async ({ toolCallId, answer }) => {
|
|
395
|
+
if (!copilotAgent) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const pauses = _collectServerPauses(copilotAgent.messages);
|
|
399
|
+
// Thrown, never returned: a caller resets its submitting state on a rejection,
|
|
400
|
+
// while a silent return leaves the card locked with the answer discarded.
|
|
401
|
+
if (!pauses.some((toolCall) => toolCall.id === toolCallId)) {
|
|
402
|
+
throw new Error(`answerInterrupt: ${toolCallId} is not a pending pause of this turn`);
|
|
403
|
+
}
|
|
404
|
+
const answers = interruptAnswersRef.current;
|
|
405
|
+
answers.set(toolCallId, answer);
|
|
406
|
+
const unanswered = pauses.filter((toolCall) => !answers.has(toolCall.id));
|
|
407
|
+
if (unanswered.length > 0) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const decisions = pauses.map((toolCall) => ({
|
|
411
|
+
type: "respond",
|
|
412
|
+
// `?? null` because a component may call `respond()` with no payload, and
|
|
413
|
+
// `JSON.stringify(undefined)` drops the key, which raises server-side.
|
|
414
|
+
message: JSON.stringify(answers.get(toolCall.id) ?? null),
|
|
415
|
+
}));
|
|
416
|
+
const sent = new Map(answers);
|
|
417
|
+
answers.clear();
|
|
418
|
+
const restore = () => sent.forEach((value, id) => answers.set(id, value));
|
|
419
|
+
let resumed = false;
|
|
420
|
+
try {
|
|
421
|
+
// Every answer goes back, not just this call's, or a resume that never
|
|
422
|
+
// happened discards the rest of the turn's cards with it.
|
|
423
|
+
resumed = await resolveInterrupt({ decisions });
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
restore();
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
if (!resumed) {
|
|
430
|
+
// A run was already in flight, so nothing was sent: `isThinking` lags
|
|
431
|
+
// `agent.isRunning` by a commit, which leaves `respond` live for a moment
|
|
432
|
+
// after a run starts.
|
|
433
|
+
restore();
|
|
434
|
+
throw new Error("answerInterrupt: a run was already in flight; answer kept for retry");
|
|
435
|
+
}
|
|
436
|
+
}, [copilotAgent, resolveInterrupt]);
|
|
349
437
|
const stopGeneration = useCallback(() => {
|
|
350
438
|
copilotStopGeneration();
|
|
351
439
|
copilotAgent?.abortRun?.();
|
|
@@ -526,8 +614,13 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
526
614
|
// resolve the parked Promise itself — it lingers in memory until page
|
|
527
615
|
// reload. Acceptable: one closure per skip, cleared on refresh, no
|
|
528
616
|
// functional impact.
|
|
617
|
+
//
|
|
618
|
+
// Only tool calls THIS frontend answers. A `getTool` miss means the tool runs
|
|
619
|
+
// on the backend (`useRenderTool` registers a renderer, not a tool), so an
|
|
620
|
+
// unanswered one is a parked graph — faking its result would tell the server
|
|
621
|
+
// the question was answered and it would never resume.
|
|
529
622
|
if (copilotAgent) {
|
|
530
|
-
const pending = _collectPendingToolCalls(copilotAgent.messages);
|
|
623
|
+
const pending = _collectPendingToolCalls(copilotAgent.messages).filter((toolCall) => Boolean(copilotkit.getTool({ toolName: toolCall.function.name })));
|
|
531
624
|
for (const toolCall of pending) {
|
|
532
625
|
copilotAgent.addMessage({
|
|
533
626
|
id: generateId(),
|
|
@@ -574,6 +667,8 @@ const ChatInitializer = ({ agentBlock, agentChatBlock, chatId, token, appId, com
|
|
|
574
667
|
attachments,
|
|
575
668
|
sendMessage,
|
|
576
669
|
sendFromInputs,
|
|
670
|
+
resolveInterrupt,
|
|
671
|
+
answerInterrupt,
|
|
577
672
|
stopGeneration,
|
|
578
673
|
addAttachments,
|
|
579
674
|
removeAttachment,
|
|
@@ -597,7 +692,7 @@ export const AgentChatRoot = ({ ...props
|
|
|
597
692
|
// memoryEnabled = false,
|
|
598
693
|
// shortTermMemory,
|
|
599
694
|
}) => {
|
|
600
|
-
const { appId, token, children, agentId, agentHarness, useAgentBlockDirectChat, agentChatId, componentIds, chatId, defaultThreadId, noPersistency, chatContext, chatContextFiles, selfEditEnabled, } = props;
|
|
695
|
+
const { appId, token, children, agentId, agentHarness, useAgentBlockDirectChat, agentChatId, componentIds, chatId, defaultThreadId, noPersistency, chatContext, chatContextFiles, selfEditEnabled, supportsServerInterrupt, } = props;
|
|
601
696
|
const socketId = websocketsService.getSocketId();
|
|
602
697
|
const headers = useMemo(() => ({
|
|
603
698
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
@@ -610,6 +705,12 @@ export const AgentChatRoot = ({ ...props
|
|
|
610
705
|
const [agentChatBlock, setAgentChatBlock] = useState(null);
|
|
611
706
|
const [agentBlock, setAgentBlock] = useState(null);
|
|
612
707
|
const [components, setComponents] = useState([]);
|
|
708
|
+
// Chat components reach the backend nowhere else on the direct-chat path.
|
|
709
|
+
// Withheld unless this surface can render a server-parked one, so an older surface
|
|
710
|
+
// keeps its browser-side pause and pays no extra block fetch.
|
|
711
|
+
const serverInterruptComponentIds = useMemo(() => supportsServerInterrupt
|
|
712
|
+
? components.filter((component) => component.userInterrupt).map((component) => component.id)
|
|
713
|
+
: [], [components, supportsServerInterrupt]);
|
|
613
714
|
const chatContextStr = chatContext == null ? undefined : typeof chatContext === "string" ? chatContext : JSON.stringify(chatContext);
|
|
614
715
|
const getAgentChatBlock = useCallback(async () => {
|
|
615
716
|
if (!agentChatId) {
|
|
@@ -692,6 +793,8 @@ export const AgentChatRoot = ({ ...props
|
|
|
692
793
|
chatContext: chatContextStr,
|
|
693
794
|
chatContextFiles,
|
|
694
795
|
...(selfEditEnabled ? { selfEditEnabled: true } : {}),
|
|
796
|
+
...(supportsServerInterrupt ? { supportsServerInterrupt: true } : {}),
|
|
797
|
+
...(serverInterruptComponentIds.length ? { serverInterruptComponentIds } : {}),
|
|
695
798
|
},
|
|
696
799
|
// Explicit false: when undefined, CopilotKit auto-enables its inspector
|
|
697
800
|
// devtools on localhost hostnames.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/bridge-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAiB,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/bridge-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAiB,KAAK,SAAS,EAAiC,MAAM,OAAO,CAAC;AAErF,KAAK,iBAAiB,CAAC,IAAI,IAAI;IAAE,GAAG,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAC/D,KAAK,eAAe,GAAG;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAC;AACxD,KAAK,mBAAmB,GAAG;IAAE,GAAG,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AAEhE,KAAK,UAAU,CAAC,IAAI,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,eAAe,GAAG,mBAAmB,CAAC;AAExF,MAAM,MAAM,WAAW,CAAC,IAAI,SAAS,OAAO,IAAI;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,EAAE,MAAM,IAAI,CAAC;IAChC,mBAAmB,EAAE,CAAC,GAAG,EAAE,IAAI,KAAK,IAAI,CAAC;IACzC,iBAAiB,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC1C,iBAAiB,EAAE,MAAM,IAAI,CAAC;CAC/B,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;AAIrB,wBAAgB,cAAc,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAgCnE;AAED,wBAAgB,cAAc,CAAC,IAAI,SAAS,OAAO,EAAE,QAAQ,SAAmB,EAAE,YAAY,SAAmB,qBAOhH"}
|
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useContext, useState } from "react";
|
|
2
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
3
3
|
const BridgeStateContext = createContext(undefined);
|
|
4
4
|
export function BridgeProvider({ children }) {
|
|
5
5
|
const [data, setData] = useState({ api: undefined, error: undefined });
|
|
6
6
|
const [isLoading, setIsLoading] = useState(false);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
// Created once (state initializer) so the connection-owning effects can list
|
|
8
|
+
// these as dependencies without ever re-running; they only close over React
|
|
9
|
+
// state setters, which are stable.
|
|
10
|
+
const [transitions] = useState(() => ({
|
|
11
|
+
onConnectionLoading: () => {
|
|
12
|
+
setIsLoading(true);
|
|
13
|
+
setData({ api: undefined, error: undefined });
|
|
14
|
+
},
|
|
15
|
+
onConnectionSuccess: (api) => {
|
|
16
|
+
setIsLoading(false);
|
|
17
|
+
setData({ api, error: undefined });
|
|
18
|
+
},
|
|
19
|
+
onConnectionError: (error) => {
|
|
20
|
+
setIsLoading(false);
|
|
21
|
+
setData({ api: undefined, error });
|
|
22
|
+
},
|
|
23
|
+
onConnectionEnded: () => {
|
|
24
|
+
setIsLoading(false);
|
|
25
|
+
setData({ api: undefined, error: undefined });
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
// Memoized so consumers re-render only when the connection state actually
|
|
29
|
+
// changes, not whenever the provider's parent re-renders (the platform side
|
|
30
|
+
// sits under a busy MobX observer).
|
|
31
|
+
const value = useMemo(() => ({ isLoading, ...transitions, ...data }), [isLoading, transitions, data]);
|
|
32
|
+
return _jsx(BridgeStateContext, { value: value, children: children });
|
|
24
33
|
}
|
|
25
34
|
export function useBridgeState(hookName = "useBridgeState", providerName = "BridgeProvider") {
|
|
26
35
|
const context = useContext(BridgeStateContext);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import "./promise-with-resolvers-polyfill.js";
|
|
1
2
|
import { type ReactNode } from "react";
|
|
2
|
-
import { IframeApi, PlatformApi } from "./types.js";
|
|
3
|
+
import type { IframeApi, PlatformApi } from "./types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Provider for the iframe (app-render) side of the bridge.
|
|
5
|
-
* Wraps the app inside the iframe and
|
|
6
|
+
* Wraps the app inside the iframe and owns the single connection to the
|
|
7
|
+
* parent platform window.
|
|
6
8
|
*
|
|
7
9
|
* Use `usePlatform()` anywhere inside to access the platform's api.
|
|
8
10
|
*/
|
|
@@ -13,8 +15,9 @@ export declare function IframeBridgeProvider({ iframeApi, children }: {
|
|
|
13
15
|
/**
|
|
14
16
|
* Hook for iframe-side code to access the platform's api and connection state.
|
|
15
17
|
*
|
|
16
|
-
* Returns `{
|
|
17
|
-
*
|
|
18
|
+
* Returns `{ platformApi, error, isLoading }`; `platformApi` is undefined
|
|
19
|
+
* until the connection is established. Read-only — the connection is owned by
|
|
20
|
+
* `IframeBridgeProvider`, so any number of components may call this.
|
|
18
21
|
*/
|
|
19
22
|
export declare function usePlatform(): {
|
|
20
23
|
platformApi: PlatformApi | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iframe-bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/iframe-bridge-provider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"iframe-bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/iframe-bridge-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,mCAAmC,CAAC;AAI3C,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;IAAE,SAAS,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAO1G;AAkBD;;;;;;GAMG;AACH,wBAAgB,WAAW;;;;EAI1B"}
|
|
@@ -1,67 +1,40 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import "./promise-with-resolvers-polyfill.js";
|
|
2
3
|
import { getDomain, getProtocol } from "@blocksdiy/blocks-client-api/envService";
|
|
3
|
-
import {
|
|
4
|
-
import { createContext, useContext, useEffect, useRef } from "react";
|
|
4
|
+
import { WindowMessenger } from "penpal";
|
|
5
5
|
import { BridgeProvider, useBridgeState } from "./bridge-provider.js";
|
|
6
|
-
|
|
6
|
+
import { useBridgeConnection } from "./use-bridge-connection.js";
|
|
7
7
|
/**
|
|
8
8
|
* Provider for the iframe (app-render) side of the bridge.
|
|
9
|
-
* Wraps the app inside the iframe and
|
|
9
|
+
* Wraps the app inside the iframe and owns the single connection to the
|
|
10
|
+
* parent platform window.
|
|
10
11
|
*
|
|
11
12
|
* Use `usePlatform()` anywhere inside to access the platform's api.
|
|
12
13
|
*/
|
|
13
14
|
export function IframeBridgeProvider({ iframeApi, children }) {
|
|
14
|
-
return (
|
|
15
|
+
return (_jsxs(BridgeProvider, { children: [_jsx(PlatformConnection, { iframeApi: iframeApi }), children] }));
|
|
16
|
+
}
|
|
17
|
+
/** Owns the penpal connection to the parent platform window. Renders nothing. */
|
|
18
|
+
function PlatformConnection({ iframeApi }) {
|
|
19
|
+
useBridgeConnection({
|
|
20
|
+
hookName: "PlatformConnection",
|
|
21
|
+
providerName: "IframeBridgeProvider",
|
|
22
|
+
methods: iframeApi,
|
|
23
|
+
createMessenger: () => new WindowMessenger({
|
|
24
|
+
remoteWindow: window.parent,
|
|
25
|
+
allowedOrigins: [new RegExp(`^${getProtocol()}://(b-\\d+\\.)?${getDomain().replace(/\./g, "\\.")}$`)],
|
|
26
|
+
}),
|
|
27
|
+
});
|
|
28
|
+
return null;
|
|
15
29
|
}
|
|
16
30
|
/**
|
|
17
31
|
* Hook for iframe-side code to access the platform's api and connection state.
|
|
18
32
|
*
|
|
19
|
-
* Returns `{
|
|
20
|
-
*
|
|
33
|
+
* Returns `{ platformApi, error, isLoading }`; `platformApi` is undefined
|
|
34
|
+
* until the connection is established. Read-only — the connection is owned by
|
|
35
|
+
* `IframeBridgeProvider`, so any number of components may call this.
|
|
21
36
|
*/
|
|
22
37
|
export function usePlatform() {
|
|
23
|
-
const { error, isLoading
|
|
24
|
-
const iframeApi = useContext(IframeApiContext);
|
|
25
|
-
const iframeApiRef = useRef(iframeApi);
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
iframeApiRef.current = iframeApi;
|
|
28
|
-
}, [iframeApi]);
|
|
29
|
-
const onConnectionSuccessRef = useRef(onConnectionSuccess);
|
|
30
|
-
useEffect(() => {
|
|
31
|
-
onConnectionSuccessRef.current = onConnectionSuccess;
|
|
32
|
-
}, [onConnectionSuccess]);
|
|
33
|
-
const onConnectionErrorRef = useRef(onConnectionError);
|
|
34
|
-
useEffect(() => {
|
|
35
|
-
onConnectionErrorRef.current = onConnectionError;
|
|
36
|
-
}, [onConnectionError]);
|
|
37
|
-
const onConnectionLoadingRef = useRef(onConnectionLoading);
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
onConnectionLoadingRef.current = onConnectionLoading;
|
|
40
|
-
}, [onConnectionLoading]);
|
|
41
|
-
const onConnectionEndedRef = useRef(onConnectionEnded);
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
onConnectionEndedRef.current = onConnectionEnded;
|
|
44
|
-
}, [onConnectionEnded]);
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
onConnectionLoadingRef.current?.();
|
|
47
|
-
const messenger = new WindowMessenger({
|
|
48
|
-
remoteWindow: window.parent,
|
|
49
|
-
allowedOrigins: [new RegExp(`^${getProtocol()}://(b-\\d+\\.)?${getDomain().replace(/\./g, "\\.")}$`)],
|
|
50
|
-
});
|
|
51
|
-
const connection = connect({
|
|
52
|
-
messenger,
|
|
53
|
-
methods: iframeApiRef.current,
|
|
54
|
-
// log: debug("iframe-window"),
|
|
55
|
-
});
|
|
56
|
-
connection.promise
|
|
57
|
-
.then((platformApi) => onConnectionSuccessRef.current(platformApi))
|
|
58
|
-
.catch((error) => {
|
|
59
|
-
onConnectionErrorRef.current(error);
|
|
60
|
-
});
|
|
61
|
-
return () => {
|
|
62
|
-
connection.destroy();
|
|
63
|
-
onConnectionEndedRef.current?.();
|
|
64
|
-
};
|
|
65
|
-
}, []);
|
|
38
|
+
const { api, error, isLoading } = useBridgeState("usePlatform", "IframeBridgeProvider");
|
|
66
39
|
return { platformApi: api, error, isLoading };
|
|
67
40
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ThemeMode, AppUpdate, PlatformApi, IframeApi, EditedElement, ElementData } from "./types.js";
|
|
2
|
-
export { BridgeProvider,
|
|
2
|
+
export { BridgeProvider, type BridgeState } from "./bridge-provider.js";
|
|
3
3
|
export { IframeBridgeProvider, usePlatform } from "./iframe-bridge-provider.js";
|
|
4
4
|
export { PlatformBridgeProvider, PlatformBridgeIframe, useIframe } from "./platform-bridge-provider.js";
|
|
5
5
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGxG,OAAO,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErE,OAAO,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// useBridgeState is deliberately not exported: it hands out the connection
|
|
2
|
+
// state mutators, which belong solely to the providers below.
|
|
3
|
+
export { BridgeProvider } from "./bridge-provider.js";
|
|
2
4
|
export { IframeBridgeProvider, usePlatform } from "./iframe-bridge-provider.js";
|
|
3
5
|
export { PlatformBridgeProvider, PlatformBridgeIframe, useIframe } from "./platform-bridge-provider.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Methods } from "penpal";
|
|
2
|
+
import { type RefObject } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* penpal holds the `methods` object it was given at connect() time and
|
|
5
|
+
* resolves every incoming call against it at call time (own properties only,
|
|
6
|
+
* walked segment by segment — nested namespaces are part of its contract).
|
|
7
|
+
* React consumers rebuild their handler objects every render, so handing one
|
|
8
|
+
* to connect() directly would pin every handler to its first-render closures —
|
|
9
|
+
* a classic source of stale-state bugs that each consumer previously had to
|
|
10
|
+
* work around with refs.
|
|
11
|
+
*
|
|
12
|
+
* This returns a stable proxy that delegates every lookup to
|
|
13
|
+
* `methodsRef.current`, so remote calls always reach the latest committed
|
|
14
|
+
* handlers — including nested method namespaces and methods added or removed
|
|
15
|
+
* after connect(). Like penpal itself, only own properties are visible;
|
|
16
|
+
* prototype (class-instance) methods are not.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createLiveMethods<TMethods extends Methods>(methodsRef: RefObject<TMethods>): TMethods;
|
|
19
|
+
//# sourceMappingURL=live-methods.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-methods.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/live-methods.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,SAAS,OAAO,EAAE,UAAU,EAAE,SAAS,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAYrG"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* penpal holds the `methods` object it was given at connect() time and
|
|
3
|
+
* resolves every incoming call against it at call time (own properties only,
|
|
4
|
+
* walked segment by segment — nested namespaces are part of its contract).
|
|
5
|
+
* React consumers rebuild their handler objects every render, so handing one
|
|
6
|
+
* to connect() directly would pin every handler to its first-render closures —
|
|
7
|
+
* a classic source of stale-state bugs that each consumer previously had to
|
|
8
|
+
* work around with refs.
|
|
9
|
+
*
|
|
10
|
+
* This returns a stable proxy that delegates every lookup to
|
|
11
|
+
* `methodsRef.current`, so remote calls always reach the latest committed
|
|
12
|
+
* handlers — including nested method namespaces and methods added or removed
|
|
13
|
+
* after connect(). Like penpal itself, only own properties are visible;
|
|
14
|
+
* prototype (class-instance) methods are not.
|
|
15
|
+
*/
|
|
16
|
+
export function createLiveMethods(methodsRef) {
|
|
17
|
+
return new Proxy({}, {
|
|
18
|
+
get: (_target, property) => Reflect.get(methodsRef.current, property),
|
|
19
|
+
has: (_target, property) => Reflect.has(methodsRef.current, property),
|
|
20
|
+
ownKeys: () => Reflect.ownKeys(methodsRef.current),
|
|
21
|
+
getOwnPropertyDescriptor: (_target, property) => {
|
|
22
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(methodsRef.current, property);
|
|
23
|
+
// Proxy invariant: a property the (empty) target lacks must be reported
|
|
24
|
+
// as configurable.
|
|
25
|
+
return descriptor && { ...descriptor, configurable: true };
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import "./promise-with-resolvers-polyfill.js";
|
|
1
2
|
import { type ReactNode } from "react";
|
|
2
3
|
import type { IframeApi, PlatformApi } from "./types.js";
|
|
3
4
|
/**
|
|
4
5
|
* Context provider for the platform (Remix) side of the bridge.
|
|
5
6
|
* Does NOT render any iframe — use `PlatformBridgeIframe` inside this provider.
|
|
6
7
|
*
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
8
|
+
* Use `useIframe()` anywhere inside to check the connection state and call
|
|
9
|
+
* methods the iframe exposes.
|
|
9
10
|
*/
|
|
10
11
|
export declare function PlatformBridgeProvider({ children }: {
|
|
11
12
|
children: ReactNode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/platform-bridge-provider.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"platform-bridge-provider.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/platform-bridge-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,mCAAmC,CAAC;AAG3C,OAAO,EAAE,KAAK,SAAS,EAAU,MAAM,OAAO,CAAC;AAG/C,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAGtD;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAE3E;AAED,KAAK,yBAAyB,GAAG;IAAE,WAAW,EAAE,WAAW,CAAA;CAAE,GAAG,IAAI,CAClE,KAAK,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAC7C,KAAK,GAAG,OAAO,CAChB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,WAAW,EAAE,GAAG,WAAW,EAAE,EAAE,yBAAyB,+BA0B9F;AAED;;;;GAIG;AACH,wBAAgB,SAAS;;;;EAQxB"}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import "./promise-with-resolvers-polyfill.js";
|
|
3
|
+
import { WindowMessenger } from "penpal";
|
|
4
|
+
import { useRef } from "react";
|
|
4
5
|
import { BridgeProvider, useBridgeState } from "./bridge-provider.js";
|
|
6
|
+
import { useBridgeConnection } from "./use-bridge-connection.js";
|
|
5
7
|
/**
|
|
6
8
|
* Context provider for the platform (Remix) side of the bridge.
|
|
7
9
|
* Does NOT render any iframe — use `PlatformBridgeIframe` inside this provider.
|
|
8
10
|
*
|
|
9
|
-
* `
|
|
10
|
-
*
|
|
11
|
+
* Use `useIframe()` anywhere inside to check the connection state and call
|
|
12
|
+
* methods the iframe exposes.
|
|
11
13
|
*/
|
|
12
14
|
export function PlatformBridgeProvider({ children }) {
|
|
13
15
|
return _jsx(BridgeProvider, { children: children });
|
|
@@ -19,57 +21,22 @@ export function PlatformBridgeProvider({ children }) {
|
|
|
19
21
|
* Accepts all standard `<iframe>` HTML attributes (`src`, `className`, `allow`, etc.).
|
|
20
22
|
*/
|
|
21
23
|
export function PlatformBridgeIframe({ platformApi, ...iframeProps }) {
|
|
22
|
-
const { onConnectionSuccess, onConnectionError, onConnectionLoading, onConnectionEnded } = useBridgeState("PlatformBridgeIframe", "PlatformBridgeProvider");
|
|
23
|
-
const connectionRef = useRef(null);
|
|
24
|
-
const platformApiRef = useRef(platformApi);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
platformApiRef.current = platformApi;
|
|
27
|
-
}, [platformApi]);
|
|
28
|
-
const onConnectionSuccessRef = useRef(onConnectionSuccess);
|
|
29
|
-
useEffect(() => {
|
|
30
|
-
onConnectionSuccessRef.current = onConnectionSuccess;
|
|
31
|
-
}, [onConnectionSuccess]);
|
|
32
|
-
const onConnectionErrorRef = useRef(onConnectionError);
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
onConnectionErrorRef.current = onConnectionError;
|
|
35
|
-
}, [onConnectionError]);
|
|
36
|
-
const onConnectionLoadingRef = useRef(onConnectionLoading);
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
onConnectionLoadingRef.current = onConnectionLoading;
|
|
39
|
-
}, [onConnectionLoading]);
|
|
40
|
-
const onConnectionEndedRef = useRef(onConnectionEnded);
|
|
41
|
-
useEffect(() => {
|
|
42
|
-
onConnectionEndedRef.current = onConnectionEnded;
|
|
43
|
-
}, [onConnectionEnded]);
|
|
44
24
|
const iframeRef = useRef(null);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// log: debug("platform-window"),
|
|
61
|
-
});
|
|
62
|
-
connectionRef.current.promise
|
|
63
|
-
.then((api) => onConnectionSuccessRef.current(api))
|
|
64
|
-
.catch((error) => {
|
|
65
|
-
connectionRef.current?.destroy();
|
|
66
|
-
onConnectionErrorRef.current(error);
|
|
67
|
-
});
|
|
68
|
-
return () => {
|
|
69
|
-
connectionRef.current?.destroy();
|
|
70
|
-
onConnectionEndedRef.current();
|
|
71
|
-
};
|
|
72
|
-
}, []);
|
|
25
|
+
useBridgeConnection({
|
|
26
|
+
hookName: "PlatformBridgeIframe",
|
|
27
|
+
providerName: "PlatformBridgeProvider",
|
|
28
|
+
methods: platformApi,
|
|
29
|
+
createMessenger: () => {
|
|
30
|
+
const iframe = iframeRef.current;
|
|
31
|
+
if (!iframe?.contentWindow) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return new WindowMessenger({
|
|
35
|
+
remoteWindow: iframe.contentWindow,
|
|
36
|
+
allowedOrigins: [new URL(iframe.src).origin],
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
});
|
|
73
40
|
return (_jsx("iframe", { ref: iframeRef, allow: "accelerometer; autoplay; camera; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; payment; usb; xr-spatial-tracking; screen-wake-lock; magnetometer; gamepad; picture-in-picture; display-capture; bluetooth;", ...iframeProps }));
|
|
74
41
|
}
|
|
75
42
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promise-with-resolvers-polyfill.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/promise-with-resolvers-polyfill.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* penpal 8 targets ES2024 and calls `Promise.withResolvers()` natively
|
|
3
|
+
* (Chrome/Edge 119+, Firefox 121+, Safari 17.4+). The iframe side of the
|
|
4
|
+
* bridge ships inside published apps that run on end-user devices we don't
|
|
5
|
+
* control, so back-fill the method where it's missing instead of raising the
|
|
6
|
+
* browser floor for every published app.
|
|
7
|
+
*/
|
|
8
|
+
if (typeof Promise.withResolvers !== "function") {
|
|
9
|
+
Promise.withResolvers = function withResolvers() {
|
|
10
|
+
let resolve;
|
|
11
|
+
let reject;
|
|
12
|
+
const promise = new Promise((res, rej) => {
|
|
13
|
+
resolve = res;
|
|
14
|
+
reject = rej;
|
|
15
|
+
});
|
|
16
|
+
return { promise, resolve, reject };
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Messenger, type Methods, PenpalError, type RemoteProxy } from "penpal";
|
|
2
|
+
/**
|
|
3
|
+
* Owns one penpal connection for the lifetime of the calling component and
|
|
4
|
+
* drives the shared bridge state through it: loading on mount, success/error
|
|
5
|
+
* from the handshake, ended on unmount. Both sides of the bridge share this
|
|
6
|
+
* hook so the subtle parts below stay identical on each side.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useBridgeConnection<TRemoteApi extends Methods>({ hookName, providerName, createMessenger, methods, }: {
|
|
9
|
+
hookName: string;
|
|
10
|
+
providerName: string;
|
|
11
|
+
/**
|
|
12
|
+
* Builds the messenger for this connection, called one tick after mount.
|
|
13
|
+
* Return undefined to skip connecting (e.g. no iframe contentWindow yet);
|
|
14
|
+
* throws surface as connection errors.
|
|
15
|
+
*/
|
|
16
|
+
createMessenger: () => Messenger | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Methods to expose to the remote. Pass the current render's object as-is —
|
|
19
|
+
* the hook forwards remote calls to the latest committed handlers (see
|
|
20
|
+
* `createLiveMethods`), so it never goes stale and never re-connects.
|
|
21
|
+
*/
|
|
22
|
+
methods: Methods;
|
|
23
|
+
}): void;
|
|
24
|
+
/**
|
|
25
|
+
* penpal gives no signal when the remote destroys an established connection —
|
|
26
|
+
* the remote proxy just starts throwing CONNECTION_DESTROYED synchronously
|
|
27
|
+
* from every method call. Wrap the proxy so those throws become rejections
|
|
28
|
+
* (matching the methods' async signatures) and report the death, instead of
|
|
29
|
+
* crashing whichever component happened to make the next call.
|
|
30
|
+
*
|
|
31
|
+
* Exported for tests.
|
|
32
|
+
*/
|
|
33
|
+
export declare function guardDestroyedProxy<TApi extends Methods>(api: RemoteProxy<TApi>, onDestroyedCall: (error: PenpalError) => void): RemoteProxy<TApi>;
|
|
34
|
+
//# sourceMappingURL=use-bridge-connection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-bridge-connection.d.ts","sourceRoot":"","sources":["../../src/iframe-communication/use-bridge-connection.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,WAAW,EACX,KAAK,WAAW,EACjB,MAAM,QAAQ,CAAC;AAehB;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,SAAS,OAAO,EAAE,EAC9D,QAAQ,EACR,YAAY,EACZ,eAAe,EACf,OAAO,GACR,EAAE;IACD,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,eAAe,EAAE,MAAM,SAAS,GAAG,SAAS,CAAC;IAC7C;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;CAClB,QAoFA;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,SAAS,OAAO,EACtD,GAAG,EAAE,WAAW,CAAC,IAAI,CAAC,EACtB,eAAe,EAAE,CAAC,KAAK,EAAE,WAAW,KAAK,IAAI,GAC5C,WAAW,CAAC,IAAI,CAAC,CAuBnB"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { connect, ErrorCode, PenpalError, } from "penpal";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useBridgeState } from "./bridge-provider.js";
|
|
4
|
+
import { createLiveMethods } from "./live-methods.js";
|
|
5
|
+
/**
|
|
6
|
+
* How long to wait for the penpal handshake before failing the connection.
|
|
7
|
+
* penpal only arms its handshake timer when a timeout is provided — without
|
|
8
|
+
* one, an iframe that never loads (bad src, CSP denial, fatal script error
|
|
9
|
+
* before the provider mounts) leaves the bridge loading forever with no error
|
|
10
|
+
* for consumers to surface.
|
|
11
|
+
*/
|
|
12
|
+
const CONNECT_TIMEOUT_MS = 60_000;
|
|
13
|
+
/**
|
|
14
|
+
* Owns one penpal connection for the lifetime of the calling component and
|
|
15
|
+
* drives the shared bridge state through it: loading on mount, success/error
|
|
16
|
+
* from the handshake, ended on unmount. Both sides of the bridge share this
|
|
17
|
+
* hook so the subtle parts below stay identical on each side.
|
|
18
|
+
*/
|
|
19
|
+
export function useBridgeConnection({ hookName, providerName, createMessenger, methods, }) {
|
|
20
|
+
const { onConnectionSuccess, onConnectionError, onConnectionLoading, onConnectionEnded } = useBridgeState(hookName, providerName);
|
|
21
|
+
// Latest committed factory and methods, so the connection effect never
|
|
22
|
+
// re-runs just because a render rebuilt them.
|
|
23
|
+
const createMessengerRef = useRef(createMessenger);
|
|
24
|
+
const methodsRef = useRef(methods);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
createMessengerRef.current = createMessenger;
|
|
27
|
+
methodsRef.current = methods;
|
|
28
|
+
}, [createMessenger, methods]);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
onConnectionLoading();
|
|
31
|
+
// Deferred one tick, cancelled by cleanup: dev StrictMode's synchronous
|
|
32
|
+
// mount -> cleanup -> remount then never creates the throwaway first
|
|
33
|
+
// connection. A destroyed connection's SYN otherwise stays on the wire and
|
|
34
|
+
// can mispair the real handshake — both sides resolve but the MessagePorts
|
|
35
|
+
// end up crossed and every call hangs (reproduced: 6/12 trials).
|
|
36
|
+
let connection;
|
|
37
|
+
// Set before cleanup destroys the connection, so the rejection that
|
|
38
|
+
// destroy() causes can be told apart from a remote-initiated teardown.
|
|
39
|
+
let destroyedByCleanup = false;
|
|
40
|
+
const connectTimer = setTimeout(() => {
|
|
41
|
+
// Synchronous setup throws (an invalid iframe src URL, penpal argument
|
|
42
|
+
// validation) would otherwise become uncaught errors inside the timer —
|
|
43
|
+
// route them into the bridge state like any other connection failure.
|
|
44
|
+
try {
|
|
45
|
+
const messenger = createMessengerRef.current();
|
|
46
|
+
if (!messenger) {
|
|
47
|
+
onConnectionEnded();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
connection = connect({
|
|
51
|
+
messenger,
|
|
52
|
+
methods: createLiveMethods(methodsRef),
|
|
53
|
+
timeout: CONNECT_TIMEOUT_MS,
|
|
54
|
+
});
|
|
55
|
+
connection.promise
|
|
56
|
+
.then((api) => {
|
|
57
|
+
const guardedApi = guardDestroyedProxy(api, (error) => {
|
|
58
|
+
// Ignore calls on a proxy this effect already tore down — the
|
|
59
|
+
// bridge state has moved on (possibly to a newer connection).
|
|
60
|
+
if (!destroyedByCleanup) {
|
|
61
|
+
onConnectionError(error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
// RemoteProxy<T> is structurally T for our promise-returning
|
|
65
|
+
// bridge APIs; TypeScript just can't prove it for an unresolved
|
|
66
|
+
// type parameter.
|
|
67
|
+
onConnectionSuccess(guardedApi);
|
|
68
|
+
})
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
// Since penpal 8, destroying a pending connection rejects its
|
|
71
|
+
// promise instead of leaking it. Only our own cleanup's destroy()
|
|
72
|
+
// is expected teardown — a CONNECTION_DESTROYED from anywhere else
|
|
73
|
+
// means the remote tore down a connection we still wanted, which
|
|
74
|
+
// is a failure like any other.
|
|
75
|
+
if (destroyedByCleanup && error instanceof PenpalError && error.code === ErrorCode.ConnectionDestroyed) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
onConnectionError(error);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
onConnectionError(error instanceof Error ? error : new Error(String(error)));
|
|
83
|
+
}
|
|
84
|
+
}, 0);
|
|
85
|
+
return () => {
|
|
86
|
+
destroyedByCleanup = true;
|
|
87
|
+
clearTimeout(connectTimer);
|
|
88
|
+
connection?.destroy();
|
|
89
|
+
onConnectionEnded();
|
|
90
|
+
};
|
|
91
|
+
// The transitions are created once by BridgeProvider, so this effect runs
|
|
92
|
+
// once per mounted connection owner.
|
|
93
|
+
}, [onConnectionSuccess, onConnectionError, onConnectionLoading, onConnectionEnded]);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* penpal gives no signal when the remote destroys an established connection —
|
|
97
|
+
* the remote proxy just starts throwing CONNECTION_DESTROYED synchronously
|
|
98
|
+
* from every method call. Wrap the proxy so those throws become rejections
|
|
99
|
+
* (matching the methods' async signatures) and report the death, instead of
|
|
100
|
+
* crashing whichever component happened to make the next call.
|
|
101
|
+
*
|
|
102
|
+
* Exported for tests.
|
|
103
|
+
*/
|
|
104
|
+
export function guardDestroyedProxy(api, onDestroyedCall) {
|
|
105
|
+
const wrap = (value) => {
|
|
106
|
+
if ((typeof value !== "function" && typeof value !== "object") || value === null) {
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return new Proxy(value, {
|
|
110
|
+
// penpal's remote proxy resolves nested method paths lazily through
|
|
111
|
+
// property gets, so wrap whatever comes back the same way.
|
|
112
|
+
get: (target, property, receiver) => wrap(Reflect.get(target, property, receiver)),
|
|
113
|
+
apply: (target, thisArg, args) => {
|
|
114
|
+
try {
|
|
115
|
+
return Reflect.apply(target, thisArg, args);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof PenpalError && error.code === ErrorCode.ConnectionDestroyed) {
|
|
119
|
+
onDestroyedCall(error);
|
|
120
|
+
return Promise.reject(error);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
};
|
|
127
|
+
return wrap(api);
|
|
128
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocksdiy/react-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "React common",
|
|
6
6
|
"keywords": [],
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@copilotkit/react-core": "1.60.1",
|
|
36
36
|
"@radix-ui/react-slot": "^1.2.3",
|
|
37
37
|
"@vapi-ai/web": "^2.5.2",
|
|
38
|
-
"penpal": "^
|
|
38
|
+
"penpal": "^8.0.0",
|
|
39
39
|
"react": "^19.2.8",
|
|
40
40
|
"use-stick-to-bottom": "1.1.4",
|
|
41
41
|
"@blocksdiy/blocks-client-api": "1.5.0"
|
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
"babel-plugin-react-compiler": "^1.0.0",
|
|
46
46
|
"eslint-plugin-react": "^7.37.5",
|
|
47
47
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
48
|
+
"jest": "^29.7.0",
|
|
49
|
+
"ts-jest": "^29.2.5",
|
|
48
50
|
"tsc-alias": "^1.8.10",
|
|
49
51
|
"typescript": "^5.9.3",
|
|
50
52
|
"@blockscom/eslint-config": "4.6.4"
|
|
@@ -55,7 +57,7 @@
|
|
|
55
57
|
"scripts": {
|
|
56
58
|
"clean": "rm -rf ./dist/ && rm -rf tsconfig.tsbuildinfo && rm -rf node_modules",
|
|
57
59
|
"build": "rm -rf dist && tsc && tsc-alias && echo $(date +%s) > .build-sentinel.json",
|
|
58
|
-
"test": "
|
|
60
|
+
"test": "jest --config test/jest.json",
|
|
59
61
|
"lint": "pnpm eslint src/",
|
|
60
62
|
"lint-fix": "pnpm eslint src/ --fix"
|
|
61
63
|
}
|