@assistant-ui/react 0.15.17 → 0.15.19
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/README.md +8 -2
- package/dist/context/providers/ThreadViewportProvider.js +2 -2
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +91 -47
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +57 -33
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +9 -4
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +2 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -8
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/message/MessageParts.js +2 -14
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
- package/dist/primitives/message/MessagePartsGrouped.js +2 -2
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js +13 -36
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +3 -1
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +36 -9
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +24 -2
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +9 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +50 -43
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -5
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +16 -15
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
- package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
- package/dist/unstable/useModelContextSnapshot.js +61 -0
- package/dist/unstable/useModelContextSnapshot.js.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +2 -8
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
- package/dist/unstable/webmcp/convertTools.d.ts +21 -0
- package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
- package/dist/unstable/webmcp/convertTools.js +161 -0
- package/dist/unstable/webmcp/convertTools.js.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js +106 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.js +29 -0
- package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
- package/dist/utils/Primitive.d.ts +10 -2
- package/dist/utils/Primitive.d.ts.map +1 -1
- package/dist/utils/Primitive.js +25 -6
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +17 -3
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/package.json +13 -13
- package/src/context/providers/ThreadViewportProvider.tsx +2 -2
- package/src/hooks/useToolCallElapsed.ts +3 -0
- package/src/index.ts +14 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
- package/src/mcp-apps/McpAppRenderer.tsx +147 -52
- package/src/mcp-apps/app-frame.test.tsx +437 -1
- package/src/mcp-apps/app-frame.tsx +76 -21
- package/src/mcp-apps/bridge.test.ts +98 -0
- package/src/mcp-apps/bridge.ts +14 -3
- package/src/mcp-apps/index.ts +5 -1
- package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
- package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
- package/src/primitives/composer/ComposerInput.test.tsx +22 -0
- package/src/primitives/composer/ComposerInput.tsx +5 -9
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
- package/src/primitives/message/MessageParts.tsx +1 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
- package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
- package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
- package/src/primitives/messagePart/MessagePartText.tsx +22 -9
- package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
- package/src/primitives/reasoning/useScrollLock.ts +15 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +160 -2
- package/src/sandbox-host/SandboxHost.tsx +50 -10
- package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
- package/src/tests/toolCallTiming.test.tsx +55 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
- package/src/unstable/useLiveCompletionAdapter.ts +38 -8
- package/src/unstable/useMentionAdapter.test.tsx +383 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useMessageStallDetection.ts +26 -19
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.ts +2 -9
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
- package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
- package/src/unstable/webmcp/convertTools.test.ts +553 -0
- package/src/unstable/webmcp/convertTools.ts +260 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.test.tsx +46 -1
- package/src/utils/Primitive.tsx +37 -7
- package/src/utils/getSelectionMessageId.test.ts +176 -1
- package/src/utils/getSelectionMessageId.ts +21 -3
- package/dist/utils/invokeCallbackSafely.d.ts +0 -5
- package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
- package/dist/utils/invokeCallbackSafely.js +0 -15
- package/dist/utils/invokeCallbackSafely.js.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
- package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.js +0 -23
- package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
- package/src/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -7,8 +7,10 @@ import { AssistantRuntimeProvider } from "../context";
|
|
|
7
7
|
import * as MessagePrimitive from "../primitives/message";
|
|
8
8
|
import * as ThreadPrimitive from "../primitives/thread";
|
|
9
9
|
import { useLocalRuntime } from "../legacy-runtime/runtime-cores/local/useLocalRuntime";
|
|
10
|
-
import type { ChatModelAdapter, ThreadMessageLike } from "../index";
|
|
11
10
|
import {
|
|
11
|
+
type ChatModelAdapter,
|
|
12
|
+
type ThreadMessageLike,
|
|
13
|
+
useExternalStoreRuntime,
|
|
12
14
|
useToolCallElapsed,
|
|
13
15
|
unstable_useMessageStallDetection,
|
|
14
16
|
} from "../index";
|
|
@@ -60,6 +62,22 @@ const RuntimeProvider: FC<
|
|
|
60
62
|
);
|
|
61
63
|
};
|
|
62
64
|
|
|
65
|
+
const ExternalRuntimeProvider: FC<
|
|
66
|
+
PropsWithChildren<{ messages: ThreadMessageLike[] }>
|
|
67
|
+
> = ({ messages, children }) => {
|
|
68
|
+
const runtime = useExternalStoreRuntime({
|
|
69
|
+
messages,
|
|
70
|
+
isRunning: true,
|
|
71
|
+
convertMessage: (message) => message,
|
|
72
|
+
onNew: async () => {},
|
|
73
|
+
});
|
|
74
|
+
return (
|
|
75
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
76
|
+
{children}
|
|
77
|
+
</AssistantRuntimeProvider>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
63
81
|
const renderHarness = async (messages: ThreadMessageLike[], probe: FC) => {
|
|
64
82
|
render(<Harness messages={messages} probe={probe} />);
|
|
65
83
|
// Flush the runtime's deferred initialization, which fake timers hold back.
|
|
@@ -83,6 +101,19 @@ const Harness: FC<{ messages: ThreadMessageLike[]; probe: FC }> = ({
|
|
|
83
101
|
</RuntimeProvider>
|
|
84
102
|
);
|
|
85
103
|
|
|
104
|
+
const ExternalHarness: FC<{
|
|
105
|
+
messages: ThreadMessageLike[];
|
|
106
|
+
probe: FC;
|
|
107
|
+
}> = ({ messages, probe: Probe }) => (
|
|
108
|
+
<ExternalRuntimeProvider messages={messages}>
|
|
109
|
+
<ThreadPrimitive.Messages
|
|
110
|
+
components={{
|
|
111
|
+
Message: () => <MessagePrimitive.Parts components={{ Text: Probe }} />,
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</ExternalRuntimeProvider>
|
|
115
|
+
);
|
|
116
|
+
|
|
86
117
|
describe("useToolCallElapsed", () => {
|
|
87
118
|
beforeEach(() => {
|
|
88
119
|
vi.useFakeTimers();
|
|
@@ -218,4 +249,27 @@ describe("unstable_useMessageStallDetection", () => {
|
|
|
218
249
|
|
|
219
250
|
expect(screen.getByTestId("stalled").textContent).toBe("false");
|
|
220
251
|
});
|
|
252
|
+
|
|
253
|
+
it("resets the timer for equal-length content changes", async () => {
|
|
254
|
+
const message = (text: string): ThreadMessageLike => ({
|
|
255
|
+
role: "assistant",
|
|
256
|
+
content: [{ type: "text", text }],
|
|
257
|
+
status: { type: "running" },
|
|
258
|
+
});
|
|
259
|
+
const view = render(
|
|
260
|
+
<ExternalHarness messages={[message("first")]} probe={StallProbe} />,
|
|
261
|
+
);
|
|
262
|
+
await act(async () => vi.advanceTimersByTimeAsync(0));
|
|
263
|
+
|
|
264
|
+
await act(async () => vi.advanceTimersByTimeAsync(1500));
|
|
265
|
+
view.rerender(
|
|
266
|
+
<ExternalHarness messages={[message("other")]} probe={StallProbe} />,
|
|
267
|
+
);
|
|
268
|
+
await act(async () => vi.advanceTimersByTimeAsync(1000));
|
|
269
|
+
|
|
270
|
+
expect(screen.getByTestId("stalled").textContent).toBe("false");
|
|
271
|
+
|
|
272
|
+
await act(async () => vi.advanceTimersByTimeAsync(1000));
|
|
273
|
+
expect(screen.getByTestId("stalled").textContent).toBe("true");
|
|
274
|
+
});
|
|
221
275
|
});
|
|
@@ -23,7 +23,8 @@ vi.mock("@assistant-ui/store", () => ({
|
|
|
23
23
|
},
|
|
24
24
|
}),
|
|
25
25
|
}));
|
|
26
|
-
vi.mock("@assistant-ui/tap", () => ({
|
|
26
|
+
vi.mock("@assistant-ui/tap", async (importOriginal) => ({
|
|
27
|
+
...(await importOriginal<typeof import("@assistant-ui/tap")>()),
|
|
27
28
|
flushTapSync: (fn: () => void) => fn(),
|
|
28
29
|
}));
|
|
29
30
|
vi.mock("../primitives/composer/trigger/TriggerPopoverRootContext", () => ({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @vitest-environment jsdom */
|
|
2
|
-
import { startTransition, Suspense } from "react";
|
|
2
|
+
import { startTransition, Suspense, useLayoutEffect } from "react";
|
|
3
3
|
import { act, render, renderHook } from "@testing-library/react";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import type { Unstable_TriggerItem } from "@assistant-ui/core";
|
|
@@ -62,6 +62,159 @@ describe("unstable_useLiveCompletionAdapter", () => {
|
|
|
62
62
|
expect(fetcher).not.toHaveBeenCalled();
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it("does not start a queued fetch after unmount", async () => {
|
|
66
|
+
const fetcher = vi.fn(async () => []);
|
|
67
|
+
const { result, unmount } = renderHook(() =>
|
|
68
|
+
unstable_useLiveCompletionAdapter({ fetcher, debounceMs: 0 }),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
result.current.adapter.search!("alice");
|
|
72
|
+
unmount();
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
|
|
75
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
76
|
+
await vi.runAllTimersAsync();
|
|
77
|
+
|
|
78
|
+
expect(fetcher).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("keeps an in-flight fetch when the debounce changes", async () => {
|
|
82
|
+
let resolve!: (items: readonly Unstable_TriggerItem[]) => void;
|
|
83
|
+
const fetcher = vi.fn(
|
|
84
|
+
() =>
|
|
85
|
+
new Promise<readonly Unstable_TriggerItem[]>((r) => {
|
|
86
|
+
resolve = r;
|
|
87
|
+
}),
|
|
88
|
+
);
|
|
89
|
+
const { result, rerender } = renderHook(
|
|
90
|
+
({ debounceMs }) =>
|
|
91
|
+
unstable_useLiveCompletionAdapter({ fetcher, debounceMs }),
|
|
92
|
+
{ initialProps: { debounceMs: 0 } },
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
await act(async () => {
|
|
96
|
+
result.current.adapter.search!("alice");
|
|
97
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
98
|
+
});
|
|
99
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
100
|
+
|
|
101
|
+
rerender({ debounceMs: 50 });
|
|
102
|
+
await act(async () => resolve([item("alice")]));
|
|
103
|
+
|
|
104
|
+
expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
|
|
105
|
+
expect(result.current.isLoading).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("replays a search queued while the adapter is hidden", async () => {
|
|
109
|
+
const fetcher = vi.fn(async () => []);
|
|
110
|
+
const suspended = new Promise<never>(() => {});
|
|
111
|
+
let committed!: ReturnType<typeof unstable_useLiveCompletionAdapter>;
|
|
112
|
+
const Harness = ({ blocked }: { blocked: boolean }) => {
|
|
113
|
+
const current = unstable_useLiveCompletionAdapter({
|
|
114
|
+
fetcher,
|
|
115
|
+
debounceMs: 0,
|
|
116
|
+
});
|
|
117
|
+
useLayoutEffect(() => {
|
|
118
|
+
committed = current;
|
|
119
|
+
}, [current]);
|
|
120
|
+
if (blocked) throw suspended;
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
const view = (blocked: boolean) => (
|
|
124
|
+
<Suspense fallback={null}>
|
|
125
|
+
<Harness blocked={blocked} />
|
|
126
|
+
</Suspense>
|
|
127
|
+
);
|
|
128
|
+
const rendered = render(view(false));
|
|
129
|
+
|
|
130
|
+
await act(async () => rendered.rerender(view(true)));
|
|
131
|
+
committed.adapter.search!("alice");
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
|
|
134
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
135
|
+
await act(async () => rendered.rerender(view(false)));
|
|
136
|
+
await act(async () => vi.advanceTimersByTimeAsync(0));
|
|
137
|
+
|
|
138
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
139
|
+
expect(fetcher).toHaveBeenCalledWith("alice");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("drops a hidden search superseded by a cached query", async () => {
|
|
143
|
+
const fetcher = vi.fn(async (query: string) => [item(query)]);
|
|
144
|
+
const suspended = new Promise<never>(() => {});
|
|
145
|
+
let committed!: ReturnType<typeof unstable_useLiveCompletionAdapter>;
|
|
146
|
+
const Harness = ({ blocked }: { blocked: boolean }) => {
|
|
147
|
+
const current = unstable_useLiveCompletionAdapter({
|
|
148
|
+
fetcher,
|
|
149
|
+
debounceMs: 0,
|
|
150
|
+
});
|
|
151
|
+
useLayoutEffect(() => {
|
|
152
|
+
committed = current;
|
|
153
|
+
}, [current]);
|
|
154
|
+
if (blocked) throw suspended;
|
|
155
|
+
return null;
|
|
156
|
+
};
|
|
157
|
+
const view = (blocked: boolean) => (
|
|
158
|
+
<Suspense fallback={null}>
|
|
159
|
+
<Harness blocked={blocked} />
|
|
160
|
+
</Suspense>
|
|
161
|
+
);
|
|
162
|
+
const rendered = render(view(false));
|
|
163
|
+
|
|
164
|
+
await act(async () => {
|
|
165
|
+
committed.adapter.search!("alice");
|
|
166
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
167
|
+
});
|
|
168
|
+
expect(committed.adapter.search!("alice")).toEqual([item("alice")]);
|
|
169
|
+
|
|
170
|
+
await act(async () => rendered.rerender(view(true)));
|
|
171
|
+
committed.adapter.search!("bob");
|
|
172
|
+
await Promise.resolve();
|
|
173
|
+
committed.adapter.search!("alice");
|
|
174
|
+
await Promise.resolve();
|
|
175
|
+
|
|
176
|
+
await act(async () => rendered.rerender(view(false)));
|
|
177
|
+
await act(async () => vi.advanceTimersByTimeAsync(0));
|
|
178
|
+
|
|
179
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
180
|
+
expect(fetcher).toHaveBeenCalledWith("alice");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("settles loading when a suspended adapter is shown again", async () => {
|
|
184
|
+
const fetcher = vi.fn(() => new Promise<never>(() => {}));
|
|
185
|
+
const suspended = new Promise<never>(() => {});
|
|
186
|
+
let committed!: ReturnType<typeof unstable_useLiveCompletionAdapter>;
|
|
187
|
+
const Harness = ({ blocked }: { blocked: boolean }) => {
|
|
188
|
+
const current = unstable_useLiveCompletionAdapter({
|
|
189
|
+
fetcher,
|
|
190
|
+
debounceMs: 0,
|
|
191
|
+
});
|
|
192
|
+
useLayoutEffect(() => {
|
|
193
|
+
committed = current;
|
|
194
|
+
}, [current]);
|
|
195
|
+
if (blocked) throw suspended;
|
|
196
|
+
return <output data-testid="status">{String(current.isLoading)}</output>;
|
|
197
|
+
};
|
|
198
|
+
const view = (blocked: boolean) => (
|
|
199
|
+
<Suspense fallback={null}>
|
|
200
|
+
<Harness blocked={blocked} />
|
|
201
|
+
</Suspense>
|
|
202
|
+
);
|
|
203
|
+
const rendered = render(view(false));
|
|
204
|
+
|
|
205
|
+
await act(async () => {
|
|
206
|
+
committed.adapter.search!("alice");
|
|
207
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
208
|
+
});
|
|
209
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
210
|
+
expect(rendered.getByTestId("status").textContent).toBe("true");
|
|
211
|
+
|
|
212
|
+
await act(async () => rendered.rerender(view(true)));
|
|
213
|
+
await act(async () => rendered.rerender(view(false)));
|
|
214
|
+
|
|
215
|
+
expect(rendered.getByTestId("status").textContent).toBe("false");
|
|
216
|
+
});
|
|
217
|
+
|
|
65
218
|
it("does not fetch when disabled and clears cached items", async () => {
|
|
66
219
|
const fetcher = vi.fn(async () => [item("a")]);
|
|
67
220
|
const { result, rerender } = renderHook(
|
|
@@ -435,6 +588,54 @@ describe("unstable_useLiveCompletionAdapter", () => {
|
|
|
435
588
|
expect(result.current.adapter.search!("alice")).toEqual([item("alice")]);
|
|
436
589
|
});
|
|
437
590
|
|
|
591
|
+
it("re-arms a failed query when its retry is hidden", async () => {
|
|
592
|
+
const fetcher = vi
|
|
593
|
+
.fn<(query: string) => Promise<readonly Unstable_TriggerItem[]>>()
|
|
594
|
+
.mockRejectedValueOnce(new Error("temporarily unavailable"))
|
|
595
|
+
.mockImplementationOnce(() => new Promise(() => {}))
|
|
596
|
+
.mockResolvedValueOnce([item("alice")]);
|
|
597
|
+
const suspended = new Promise<never>(() => {});
|
|
598
|
+
let committed!: ReturnType<typeof unstable_useLiveCompletionAdapter>;
|
|
599
|
+
const Harness = ({ blocked }: { blocked: boolean }) => {
|
|
600
|
+
const current = unstable_useLiveCompletionAdapter({
|
|
601
|
+
fetcher,
|
|
602
|
+
debounceMs: 0,
|
|
603
|
+
});
|
|
604
|
+
useLayoutEffect(() => {
|
|
605
|
+
committed = current;
|
|
606
|
+
}, [current]);
|
|
607
|
+
if (blocked) throw suspended;
|
|
608
|
+
return null;
|
|
609
|
+
};
|
|
610
|
+
const view = (blocked: boolean) => (
|
|
611
|
+
<Suspense fallback={null}>
|
|
612
|
+
<Harness blocked={blocked} />
|
|
613
|
+
</Suspense>
|
|
614
|
+
);
|
|
615
|
+
const rendered = render(view(false));
|
|
616
|
+
|
|
617
|
+
await act(async () => {
|
|
618
|
+
committed.adapter.search!("alice");
|
|
619
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
620
|
+
});
|
|
621
|
+
expect(fetcher).toHaveBeenCalledOnce();
|
|
622
|
+
|
|
623
|
+
await act(async () => {
|
|
624
|
+
committed.adapter.search!("alice");
|
|
625
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
626
|
+
});
|
|
627
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
628
|
+
|
|
629
|
+
await act(async () => rendered.rerender(view(true)));
|
|
630
|
+
await act(async () => rendered.rerender(view(false)));
|
|
631
|
+
await act(async () => {
|
|
632
|
+
committed.adapter.search!("alice");
|
|
633
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
expect(fetcher).toHaveBeenCalledTimes(3);
|
|
637
|
+
});
|
|
638
|
+
|
|
438
639
|
it("drops an in-flight fetch when the query returns to a cached value", async () => {
|
|
439
640
|
const resolvers: Record<
|
|
440
641
|
string,
|
|
@@ -84,6 +84,8 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
84
84
|
const pendingQueryRef = useRef<string | null>(null);
|
|
85
85
|
const retryableQueryRef = useRef<string | null>(null);
|
|
86
86
|
const pendingRetryQueryRef = useRef<string | null>(null);
|
|
87
|
+
const inactiveRef = useRef(true);
|
|
88
|
+
const deferredQueryRef = useRef<string | null>(null);
|
|
87
89
|
|
|
88
90
|
const cancelTimer = useCallback(() => {
|
|
89
91
|
if (timerRef.current !== null) {
|
|
@@ -102,6 +104,10 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
102
104
|
const scheduleFetch = useCallback(
|
|
103
105
|
(query: string) => {
|
|
104
106
|
if (!enabled) return;
|
|
107
|
+
if (inactiveRef.current) {
|
|
108
|
+
deferredQueryRef.current = query;
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
105
111
|
if (pendingQueryRef.current === query) return;
|
|
106
112
|
rearmPendingRetry();
|
|
107
113
|
if (retryableQueryRef.current === query) {
|
|
@@ -136,6 +142,11 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
136
142
|
[enabled, debounceMs, cancelTimer, rearmPendingRetry],
|
|
137
143
|
);
|
|
138
144
|
|
|
145
|
+
const scheduleFetchRef = useRef(scheduleFetch);
|
|
146
|
+
useLayoutEffect(() => {
|
|
147
|
+
scheduleFetchRef.current = scheduleFetch;
|
|
148
|
+
}, [scheduleFetch]);
|
|
149
|
+
|
|
139
150
|
const invalidatePending = useCallback(() => {
|
|
140
151
|
rearmPendingRetry();
|
|
141
152
|
cancelTimer();
|
|
@@ -162,7 +173,18 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
162
173
|
);
|
|
163
174
|
}, [enabled, invalidatePending]);
|
|
164
175
|
|
|
165
|
-
|
|
176
|
+
// Render-time searches can outlive an abandoned render, so they only arm
|
|
177
|
+
// request work after this hook commits.
|
|
178
|
+
useLayoutEffect(() => {
|
|
179
|
+
inactiveRef.current = false;
|
|
180
|
+
const deferredQuery = deferredQueryRef.current;
|
|
181
|
+
deferredQueryRef.current = null;
|
|
182
|
+
if (deferredQuery !== null) scheduleFetchRef.current(deferredQuery);
|
|
183
|
+
return () => {
|
|
184
|
+
inactiveRef.current = true;
|
|
185
|
+
invalidatePending();
|
|
186
|
+
};
|
|
187
|
+
}, [invalidatePending]);
|
|
166
188
|
|
|
167
189
|
// Arm retries only after the failed state commits. Arming during rejection
|
|
168
190
|
// would let the failure render immediately schedule another request.
|
|
@@ -179,13 +201,21 @@ export function unstable_useLiveCompletionAdapter(
|
|
|
179
201
|
// queueMicrotask so they are not dispatched while another component renders.
|
|
180
202
|
if (query !== state.query || retryableQueryRef.current === query) {
|
|
181
203
|
queueMicrotask(() => scheduleFetch(query));
|
|
182
|
-
} else
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
} else {
|
|
205
|
+
queueMicrotask(() => {
|
|
206
|
+
if (
|
|
207
|
+
deferredQueryRef.current !== null &&
|
|
208
|
+
deferredQueryRef.current !== query
|
|
209
|
+
) {
|
|
210
|
+
deferredQueryRef.current = null;
|
|
211
|
+
}
|
|
212
|
+
if (
|
|
213
|
+
pendingQueryRef.current !== null &&
|
|
214
|
+
pendingQueryRef.current !== query
|
|
215
|
+
) {
|
|
216
|
+
invalidatePending();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
189
219
|
}
|
|
190
220
|
return state.items;
|
|
191
221
|
},
|