@assistant-ui/core 0.3.17 → 0.3.18
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/adapters/attachment.d.ts.map +1 -1
- package/dist/adapters/attachment.js +5 -4
- package/dist/adapters/attachment.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +3 -2
- package/dist/internal.js +3 -2
- package/dist/model-context/frame/host.d.ts.map +1 -1
- package/dist/model-context/frame/host.js +10 -4
- package/dist/model-context/frame/host.js.map +1 -1
- package/dist/model-context/frame/provider.d.ts +2 -0
- package/dist/model-context/frame/provider.d.ts.map +1 -1
- package/dist/model-context/frame/provider.js +30 -7
- package/dist/model-context/frame/provider.js.map +1 -1
- package/dist/model-context/interactable-composer-metadata.d.ts.map +1 -1
- package/dist/model-context/interactable-composer-metadata.js +16 -9
- package/dist/model-context/interactable-composer-metadata.js.map +1 -1
- package/dist/model-context/registry.d.ts.map +1 -1
- package/dist/model-context/registry.js +3 -5
- package/dist/model-context/registry.js.map +1 -1
- package/dist/model-context/types.d.ts.map +1 -1
- package/dist/model-context/types.js +5 -4
- package/dist/model-context/types.js.map +1 -1
- package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
- package/dist/react/client/InMemoryThreadList.js +62 -56
- package/dist/react/client/InMemoryThreadList.js.map +1 -1
- package/dist/react/client/Interactables.d.ts.map +1 -1
- package/dist/react/client/Interactables.js +24 -27
- package/dist/react/client/Interactables.js.map +1 -1
- package/dist/react/client/RemoteThreadList.d.ts +1 -0
- package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
- package/dist/react/client/RemoteThreadList.js +151 -141
- package/dist/react/client/RemoteThreadList.js.map +1 -1
- package/dist/react/client/Tools.d.ts.map +1 -1
- package/dist/react/client/Tools.js +18 -16
- package/dist/react/client/Tools.js.map +1 -1
- package/dist/react/interactables-legacy/Interactables.js +242 -121
- package/dist/react/interactables-legacy/Interactables.js.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -1
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +11 -9
- package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -1
- package/dist/react/model-context/makeAssistantTool.d.ts +2 -2
- package/dist/react/model-context/makeAssistantTool.js +1 -1
- package/dist/react/model-context/makeAssistantTool.js.map +1 -1
- package/dist/react/model-context/useAssistantTool.d.ts +2 -2
- package/dist/react/model-context/useAssistantTool.js +1 -1
- package/dist/react/model-context/useAssistantTool.js.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.d.ts.map +1 -1
- package/dist/react/model-context/useToolArgsStatus.js +2 -1
- package/dist/react/model-context/useToolArgsStatus.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +16 -8
- package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +5 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +103 -103
- package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.d.ts +1 -0
- package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
- package/dist/react/runtimes/RemoteThreadResource.js +13 -11
- package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +4 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +401 -34
- package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.d.ts.map +1 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js +2 -1
- package/dist/react/runtimes/cloud/createCloudThreadListAdapter.js.map +1 -1
- package/dist/react/runtimes/useExternalStoreRuntime.js +75 -46
- package/dist/react/runtimes/useExternalStoreRuntime.js.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.d.ts.map +1 -1
- package/dist/react/runtimes/useRemoteThreadListRuntime.js +63 -23
- package/dist/react/runtimes/useRemoteThreadListRuntime.js.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.d.ts +7 -3
- package/dist/runtime/api/thread-list-item-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-item-runtime.js +2 -2
- package/dist/runtime/api/thread-list-item-runtime.js.map +1 -1
- package/dist/runtime/api/thread-list-runtime.d.ts +3 -0
- package/dist/runtime/api/thread-list-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-list-runtime.js +18 -14
- package/dist/runtime/api/thread-list-runtime.js.map +1 -1
- package/dist/runtime/api/thread-runtime.d.ts +4 -0
- package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
- package/dist/runtime/api/thread-runtime.js +1 -1
- package/dist/runtime/api/thread-runtime.js.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-composer-runtime-core.js +16 -12
- package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
- package/dist/runtime/base/base-thread-runtime-core.js +6 -5
- package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
- package/dist/runtime/interfaces/composer-runtime-core.d.ts +12 -5
- package/dist/runtime/interfaces/composer-runtime-core.d.ts.map +1 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts +4 -1
- package/dist/runtime/interfaces/thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtime/utils/external-message-conversion.js +1 -1
- package/dist/runtime/utils/external-message-conversion.js.map +1 -1
- package/dist/runtime/utils/message-repository.d.ts +1 -0
- package/dist/runtime/utils/message-repository.d.ts.map +1 -1
- package/dist/runtime/utils/message-repository.js +15 -6
- package/dist/runtime/utils/message-repository.js.map +1 -1
- package/dist/runtime/utils/thread-message-like.js +1 -1
- package/dist/runtime/utils/thread-message-like.js.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +21 -27
- package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
- package/dist/runtimes/internal.d.ts +2 -2
- package/dist/runtimes/internal.js +2 -2
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +18 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/remote-thread-state.js +130 -42
- package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
- package/dist/runtimes/remote-thread-list/title-generation.d.ts +49 -0
- package/dist/runtimes/remote-thread-list/title-generation.d.ts.map +1 -0
- package/dist/runtimes/remote-thread-list/title-generation.js +197 -0
- package/dist/runtimes/remote-thread-list/title-generation.js.map +1 -0
- package/dist/runtimes/remote-thread-list/title.d.ts +3 -1
- package/dist/runtimes/remote-thread-list/title.d.ts.map +1 -1
- package/dist/runtimes/remote-thread-list/title.js +9 -1
- package/dist/runtimes/remote-thread-list/title.js.map +1 -1
- package/dist/runtimes/remote-thread-list/types.d.ts +10 -0
- package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
- package/dist/store/clients/external-thread.d.ts.map +1 -1
- package/dist/store/clients/external-thread.js +19 -3
- package/dist/store/clients/external-thread.js.map +1 -1
- package/dist/store/clients/model-context-client.d.ts.map +1 -1
- package/dist/store/clients/model-context-client.js +2 -1
- package/dist/store/clients/model-context-client.js.map +1 -1
- package/dist/store/clients/single-thread-list.d.ts.map +1 -1
- package/dist/store/clients/single-thread-list.js +1 -0
- package/dist/store/clients/single-thread-list.js.map +1 -1
- package/dist/store/clients/suggestions.d.ts.map +1 -1
- package/dist/store/clients/suggestions.js +5 -4
- package/dist/store/clients/suggestions.js.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/composer-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/composer-runtime-client.js +97 -61
- package/dist/store/runtime-clients/composer-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.d.ts +1 -0
- package/dist/store/runtime-clients/message-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/message-runtime-client.js +221 -144
- package/dist/store/runtime-clients/message-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js +1 -1
- package/dist/store/runtime-clients/thread-list-item-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-list-runtime-client.js +54 -52
- package/dist/store/runtime-clients/thread-list-runtime-client.js.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.d.ts.map +1 -1
- package/dist/store/runtime-clients/thread-runtime-client.js +193 -121
- package/dist/store/runtime-clients/thread-runtime-client.js.map +1 -1
- package/dist/store/scopes/composer.d.ts +10 -2
- package/dist/store/scopes/composer.d.ts.map +1 -1
- package/dist/store/scopes/message.d.ts +30 -1
- package/dist/store/scopes/message.d.ts.map +1 -1
- package/dist/store/scopes/thread-list-item.d.ts +2 -2
- package/dist/store/scopes/thread-list-item.d.ts.map +1 -1
- package/dist/store/scopes/thread.d.ts +8 -0
- package/dist/store/scopes/thread.d.ts.map +1 -1
- package/dist/store/scopes/threads.d.ts +1 -0
- package/dist/store/scopes/threads.d.ts.map +1 -1
- package/dist/types/attachment.d.ts +2 -1
- package/dist/types/attachment.d.ts.map +1 -1
- package/dist/types/attachment.js +2 -1
- package/dist/types/attachment.js.map +1 -1
- package/dist/utils/abortable-iterable.d.ts +19 -0
- package/dist/utils/abortable-iterable.d.ts.map +1 -0
- package/dist/utils/abortable-iterable.js +72 -0
- package/dist/utils/abortable-iterable.js.map +1 -0
- package/dist/utils/composite-context-provider.d.ts.map +1 -1
- package/dist/utils/composite-context-provider.js +20 -2
- package/dist/utils/composite-context-provider.js.map +1 -1
- package/dist/utils/record.d.ts +11 -0
- package/dist/utils/record.d.ts.map +1 -0
- package/dist/utils/record.js +12 -0
- package/dist/utils/record.js.map +1 -0
- package/package.json +10 -10
- package/src/adapters/attachment.test.ts +50 -1
- package/src/adapters/attachment.ts +7 -4
- package/src/index.ts +1 -0
- package/src/internal.ts +4 -0
- package/src/model-context/frame/host.test.ts +24 -0
- package/src/model-context/frame/host.ts +10 -4
- package/src/model-context/frame/provider.test.ts +71 -0
- package/src/model-context/frame/provider.ts +37 -7
- package/src/model-context/interactable-composer-metadata.test.ts +28 -0
- package/src/model-context/interactable-composer-metadata.ts +9 -3
- package/src/model-context/registry.test.ts +12 -0
- package/src/model-context/registry.ts +3 -2
- package/src/model-context/types.test.ts +25 -0
- package/src/model-context/types.ts +9 -4
- package/src/react/RuntimeAdapter.test.ts +34 -0
- package/src/react/client/InMemoryThreadList.ts +1 -0
- package/src/react/client/Interactables.test.ts +28 -0
- package/src/react/client/Interactables.ts +15 -13
- package/src/react/client/RemoteThreadList.background.test.ts +148 -6
- package/src/react/client/RemoteThreadList.concurrent.test.tsx +42 -1
- package/src/react/client/RemoteThreadList.test.ts +345 -3
- package/src/react/client/RemoteThreadList.ts +166 -110
- package/src/react/client/Tools.ts +22 -11
- package/src/react/interactables-legacy/Interactables.ts +16 -16
- package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +9 -10
- package/src/react/model-context/makeAssistantTool.ts +2 -2
- package/src/react/model-context/useAssistantTool.ts +2 -2
- package/src/react/model-context/useToolArgsStatus.test.tsx +30 -0
- package/src/react/model-context/useToolArgsStatus.ts +2 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.destroy.test.tsx +172 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.running.test.tsx +33 -0
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.test.tsx +7 -1
- package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +28 -5
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.load-error.test.ts +132 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.switch-delete.test.ts +105 -0
- package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +136 -111
- package/src/react/runtimes/RemoteThreadResource.test.ts +9 -0
- package/src/react/runtimes/RemoteThreadResource.ts +19 -12
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +483 -6
- package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +504 -68
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.test.ts +15 -0
- package/src/react/runtimes/cloud/createCloudThreadListAdapter.ts +1 -0
- package/src/react/runtimes/useExternalStoreRuntime.lifecycle.test.tsx +41 -0
- package/src/react/runtimes/useExternalStoreRuntime.ts +10 -4
- package/src/react/runtimes/useLocalRuntime.test.tsx +20 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.load-error.test.tsx +108 -0
- package/src/react/runtimes/useRemoteThreadListRuntime.ts +33 -0
- package/src/runtime/api/message-runtime.test.ts +75 -0
- package/src/runtime/api/thread-list-item-runtime.ts +10 -3
- package/src/runtime/api/thread-list-runtime.ts +21 -11
- package/src/runtime/api/thread-runtime.test.ts +78 -0
- package/src/runtime/api/thread-runtime.ts +8 -1
- package/src/runtime/base/base-composer-runtime-core.ts +36 -8
- package/src/runtime/base/base-thread-runtime-core.test.ts +55 -1
- package/src/runtime/base/base-thread-runtime-core.ts +6 -4
- package/src/runtime/interfaces/composer-runtime-core.ts +10 -5
- package/src/runtime/interfaces/thread-list-runtime-core.ts +5 -1
- package/src/runtime/utils/external-message-conversion.test.ts +68 -0
- package/src/runtime/utils/external-message-conversion.ts +1 -1
- package/src/runtime/utils/message-repository.ts +29 -17
- package/src/runtime/utils/thread-message-like.ts +1 -1
- package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +18 -13
- package/src/runtimes/internal.ts +1 -0
- package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +128 -2
- package/src/runtimes/remote-thread-list/remote-thread-state.ts +182 -42
- package/src/runtimes/remote-thread-list/title-generation.test.ts +602 -0
- package/src/runtimes/remote-thread-list/title-generation.ts +329 -0
- package/src/runtimes/remote-thread-list/title.test.ts +62 -0
- package/src/runtimes/remote-thread-list/title.ts +13 -0
- package/src/runtimes/remote-thread-list/types.ts +10 -0
- package/src/store/clients/external-thread.ts +48 -3
- package/src/store/clients/model-context-client.ts +5 -1
- package/src/store/clients/single-thread-list.ts +1 -0
- package/src/store/clients/suggestions.test.ts +40 -0
- package/src/store/clients/suggestions.ts +13 -10
- package/src/store/runtime-clients/composer-runtime-client.ts +42 -9
- package/src/store/runtime-clients/message-runtime-client.ts +54 -6
- package/src/store/runtime-clients/thread-list-item-runtime-client.ts +4 -2
- package/src/store/runtime-clients/thread-list-runtime-client.ts +1 -0
- package/src/store/runtime-clients/thread-runtime-client.ts +58 -10
- package/src/store/scopes/composer.ts +16 -4
- package/src/store/scopes/message.ts +14 -0
- package/src/store/scopes/thread-list-item.ts +5 -2
- package/src/store/scopes/thread.ts +4 -0
- package/src/store/scopes/threads.ts +1 -0
- package/src/tests/MessageRepository.test.ts +116 -0
- package/src/tests/OptimisticState-list-race.test.ts +1 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-initialize-adapter-change.test.ts +38 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +47 -0
- package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +89 -0
- package/src/tests/attachment-adapters.test.ts +14 -0
- package/src/tests/base-composer-runtime-core.test.ts +39 -0
- package/src/tests/external-store-thread-list-runtime-core.test.ts +83 -0
- package/src/tests/external-store-thread-runtime-core-adapter.test.ts +27 -18
- package/src/tests/external-thread-attachments.test.tsx +139 -0
- package/src/tests/external-thread-parity.test.tsx +23 -0
- package/src/tests/remote-thread-list-isLoading.test.ts +1 -0
- package/src/tests/thread-list-runtime-getLoadThreadsPromise.test.ts +1 -0
- package/src/tests/thread-switch-events.test.tsx +78 -0
- package/src/tests/tools-scope-migration.test.tsx +28 -0
- package/src/types/attachment.ts +4 -0
- package/src/utils/abortable-iterable.test.ts +213 -0
- package/src/utils/abortable-iterable.ts +93 -0
- package/src/utils/composite-context-provider.test.ts +25 -0
- package/src/utils/composite-context-provider.ts +23 -2
- package/src/utils/record.test.ts +48 -0
- package/src/utils/record.ts +11 -0
|
@@ -23,6 +23,34 @@ afterEach(() => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
describe("Tools model-context registration", () => {
|
|
26
|
+
it("registers and removes a tool UI named __proto__", async () => {
|
|
27
|
+
let aui!: AnyClient;
|
|
28
|
+
const Harness = () => {
|
|
29
|
+
aui = useAui({ tools: Tools({}) } as never);
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
render(<Harness />);
|
|
33
|
+
|
|
34
|
+
let remove!: () => void;
|
|
35
|
+
await act(async () => {
|
|
36
|
+
remove = aui.tools().setToolUI("__proto__", () => null);
|
|
37
|
+
await vi.waitFor(() => {
|
|
38
|
+
const toolUIs = aui.tools().getState().toolUIs;
|
|
39
|
+
expect(Object.hasOwn(toolUIs, "__proto__")).toBe(true);
|
|
40
|
+
expect(toolUIs.__proto__).toHaveLength(1);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await act(async () => {
|
|
45
|
+
remove();
|
|
46
|
+
await vi.waitFor(() =>
|
|
47
|
+
expect(Object.hasOwn(aui.tools().getState().toolUIs, "__proto__")).toBe(
|
|
48
|
+
false,
|
|
49
|
+
),
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
26
54
|
it("migrates to a structurally replaced modelContext scope", async () => {
|
|
27
55
|
let aui!: AnyClient;
|
|
28
56
|
const Harness = ({ generation }: { generation: number }) => {
|
package/src/types/attachment.ts
CHANGED
|
@@ -57,3 +57,7 @@ export const isCreateAttachment = (
|
|
|
57
57
|
attachment: File | CreateAttachment,
|
|
58
58
|
): attachment is CreateAttachment =>
|
|
59
59
|
"content" in attachment && !("lastModified" in attachment);
|
|
60
|
+
|
|
61
|
+
export const isAttachmentComplete = (
|
|
62
|
+
attachment: Attachment,
|
|
63
|
+
): attachment is CompleteAttachment => attachment.status.type === "complete";
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { abortableIterable, openAbortableIterable } from "./abortable-iterable";
|
|
3
|
+
|
|
4
|
+
const deferred = <T>() => {
|
|
5
|
+
let resolve!: (value: T) => void;
|
|
6
|
+
const promise = new Promise<T>((r) => {
|
|
7
|
+
resolve = r;
|
|
8
|
+
});
|
|
9
|
+
return { promise, resolve };
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const collect = async <T>(iterable: AsyncIterable<T>) => {
|
|
13
|
+
const seen: T[] = [];
|
|
14
|
+
for await (const value of iterable) seen.push(value);
|
|
15
|
+
return seen;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("abortableIterable", () => {
|
|
19
|
+
it("settles an opening stream on abort and finalizes a late iterable", async () => {
|
|
20
|
+
const opened = deferred<AsyncIterable<number>>();
|
|
21
|
+
const finalize = vi.fn(async () => ({
|
|
22
|
+
done: true as const,
|
|
23
|
+
value: undefined,
|
|
24
|
+
}));
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
|
|
27
|
+
const opening = openAbortableIterable(opened.promise, controller.signal);
|
|
28
|
+
controller.abort();
|
|
29
|
+
|
|
30
|
+
await expect(opening).resolves.toBeUndefined();
|
|
31
|
+
opened.resolve({
|
|
32
|
+
[Symbol.asyncIterator]: () => ({
|
|
33
|
+
next: () => Promise.resolve({ done: true, value: undefined }),
|
|
34
|
+
return: finalize,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
await vi.waitFor(() => expect(finalize).toHaveBeenCalledTimes(1));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("passes a stream that completes on its own through untouched", async () => {
|
|
41
|
+
async function* source() {
|
|
42
|
+
yield 1;
|
|
43
|
+
yield 2;
|
|
44
|
+
}
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
await collect(abortableIterable(source(), controller.signal)),
|
|
49
|
+
).toEqual([1, 2]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// the reason this wrapper exists: the source ignores the signal and parks on
|
|
53
|
+
// its own work, so nothing but the abort can end the loop
|
|
54
|
+
it("ends the loop while the source is parked mid-chunk", async () => {
|
|
55
|
+
const hang = deferred<void>();
|
|
56
|
+
const finallyRan = vi.fn();
|
|
57
|
+
async function* source() {
|
|
58
|
+
try {
|
|
59
|
+
yield 1;
|
|
60
|
+
await hang.promise;
|
|
61
|
+
yield 2;
|
|
62
|
+
} finally {
|
|
63
|
+
finallyRan();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const controller = new AbortController();
|
|
67
|
+
|
|
68
|
+
const seen: number[] = [];
|
|
69
|
+
const consuming = (async () => {
|
|
70
|
+
for await (const value of abortableIterable(
|
|
71
|
+
source(),
|
|
72
|
+
controller.signal,
|
|
73
|
+
)) {
|
|
74
|
+
seen.push(value);
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
// let the loop take the first chunk and park awaiting the second
|
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
79
|
+
|
|
80
|
+
controller.abort();
|
|
81
|
+
|
|
82
|
+
// settles even though the source never does
|
|
83
|
+
await consuming;
|
|
84
|
+
expect(seen).toEqual([1]);
|
|
85
|
+
expect(finallyRan).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("finalizes a source still suspended at a yield when the signal aborts", async () => {
|
|
89
|
+
const finallyRan = vi.fn();
|
|
90
|
+
async function* source() {
|
|
91
|
+
try {
|
|
92
|
+
yield 1;
|
|
93
|
+
yield 2;
|
|
94
|
+
} finally {
|
|
95
|
+
finallyRan();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
|
|
100
|
+
const seen: number[] = [];
|
|
101
|
+
for await (const value of abortableIterable(source(), controller.signal)) {
|
|
102
|
+
seen.push(value);
|
|
103
|
+
controller.abort();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
expect(seen).toEqual([1]);
|
|
107
|
+
expect(finallyRan).toHaveBeenCalledTimes(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("reports a stream aborted before the first read as exhausted", async () => {
|
|
111
|
+
const next = vi.fn();
|
|
112
|
+
const controller = new AbortController();
|
|
113
|
+
controller.abort();
|
|
114
|
+
|
|
115
|
+
const seen = await collect(
|
|
116
|
+
abortableIterable(
|
|
117
|
+
{
|
|
118
|
+
[Symbol.asyncIterator]: () => ({ next }),
|
|
119
|
+
} as AsyncIterable<number>,
|
|
120
|
+
controller.signal,
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(seen).toEqual([]);
|
|
125
|
+
expect(next).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("propagates a source failure", async () => {
|
|
129
|
+
async function* source() {
|
|
130
|
+
yield 1;
|
|
131
|
+
throw new Error("stream failed");
|
|
132
|
+
}
|
|
133
|
+
const controller = new AbortController();
|
|
134
|
+
|
|
135
|
+
await expect(
|
|
136
|
+
collect(abortableIterable(source(), controller.signal)),
|
|
137
|
+
).rejects.toThrow("stream failed");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("finalizes a source after its next call rejects", async () => {
|
|
141
|
+
const finalize = vi.fn(async () => ({
|
|
142
|
+
done: true as const,
|
|
143
|
+
value: undefined,
|
|
144
|
+
}));
|
|
145
|
+
const source: AsyncIterable<number> = {
|
|
146
|
+
[Symbol.asyncIterator]: () => ({
|
|
147
|
+
next: () => Promise.reject(new Error("stream failed")),
|
|
148
|
+
return: finalize,
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
const controller = new AbortController();
|
|
152
|
+
|
|
153
|
+
await expect(
|
|
154
|
+
collect(abortableIterable(source, controller.signal)),
|
|
155
|
+
).rejects.toThrow("stream failed");
|
|
156
|
+
expect(finalize).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("finalizes the source when the consumer breaks out", async () => {
|
|
160
|
+
const finallyRan = vi.fn();
|
|
161
|
+
async function* source() {
|
|
162
|
+
try {
|
|
163
|
+
yield 1;
|
|
164
|
+
yield 2;
|
|
165
|
+
} finally {
|
|
166
|
+
finallyRan();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const controller = new AbortController();
|
|
170
|
+
|
|
171
|
+
for await (const _ of abortableIterable(source(), controller.signal)) {
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// finalized without the break waiting on it
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
177
|
+
expect(finallyRan).toHaveBeenCalledTimes(1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("does not let a source that parks in its own cleanup stall the break", async () => {
|
|
181
|
+
const hang = deferred<void>();
|
|
182
|
+
async function* source() {
|
|
183
|
+
try {
|
|
184
|
+
yield 1;
|
|
185
|
+
yield 2;
|
|
186
|
+
} finally {
|
|
187
|
+
await hang.promise;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const controller = new AbortController();
|
|
191
|
+
|
|
192
|
+
// resolves even though the source's finally never does
|
|
193
|
+
for await (const _ of abortableIterable(source(), controller.signal)) {
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("releases its abort listener after each chunk", async () => {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const added = vi.spyOn(controller.signal, "addEventListener");
|
|
201
|
+
const removed = vi.spyOn(controller.signal, "removeEventListener");
|
|
202
|
+
async function* source() {
|
|
203
|
+
yield 1;
|
|
204
|
+
yield 2;
|
|
205
|
+
yield 3;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await collect(abortableIterable(source(), controller.signal));
|
|
209
|
+
|
|
210
|
+
expect(added).toHaveBeenCalledTimes(removed.mock.calls.length);
|
|
211
|
+
expect(added.mock.calls.length).toBeGreaterThan(0);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const done = <T>(): IteratorResult<T> => ({ done: true, value: undefined });
|
|
2
|
+
|
|
3
|
+
/** Resolves at cancellation, so an await on a stream can stop being the only way out. */
|
|
4
|
+
const whenAborted = (signal: AbortSignal): Promise<undefined> =>
|
|
5
|
+
new Promise((resolve) => {
|
|
6
|
+
if (signal.aborted) return resolve(undefined);
|
|
7
|
+
signal.addEventListener("abort", () => resolve(undefined), { once: true });
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const openAbortableIterable = async <T>(
|
|
11
|
+
source: AsyncIterable<T> | Promise<AsyncIterable<T>>,
|
|
12
|
+
signal: AbortSignal,
|
|
13
|
+
): Promise<AsyncIterable<T> | undefined> => {
|
|
14
|
+
const opened = Promise.resolve(source);
|
|
15
|
+
const iterable = await Promise.race([opened, whenAborted(signal)]);
|
|
16
|
+
if (!iterable) {
|
|
17
|
+
void opened
|
|
18
|
+
.then((late) => late[Symbol.asyncIterator]().return?.(undefined))
|
|
19
|
+
.catch(() => {});
|
|
20
|
+
}
|
|
21
|
+
return iterable;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Presents a stream as exhausted once the signal aborts, so a consumer stops
|
|
26
|
+
* waiting on it.
|
|
27
|
+
*
|
|
28
|
+
* A caller's stream is free to ignore the abort signal it is handed. One that
|
|
29
|
+
* also stops yielding, because it is awaiting its own work, leaves `for await`
|
|
30
|
+
* parked on `next()` with nothing left to wake it: the run never settles and
|
|
31
|
+
* whatever is serialized behind it never starts. Reporting the stream as
|
|
32
|
+
* exhausted ends the loop at cancellation instead.
|
|
33
|
+
*
|
|
34
|
+
* The source is finalized on the way out, but never awaited, since a stream
|
|
35
|
+
* that is already parked would not settle that either.
|
|
36
|
+
*/
|
|
37
|
+
export const abortableIterable = <T>(
|
|
38
|
+
source: AsyncIterable<T>,
|
|
39
|
+
signal: AbortSignal,
|
|
40
|
+
): AsyncIterable<T> => ({
|
|
41
|
+
[Symbol.asyncIterator]: () => {
|
|
42
|
+
const iterator = source[Symbol.asyncIterator]();
|
|
43
|
+
const finalize = () => {
|
|
44
|
+
try {
|
|
45
|
+
iterator.return?.(undefined)?.catch(() => {});
|
|
46
|
+
} catch {
|
|
47
|
+
// a source that throws on finalize has nothing left to clean up
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
[Symbol.asyncIterator]() {
|
|
53
|
+
return this;
|
|
54
|
+
},
|
|
55
|
+
next: () => {
|
|
56
|
+
if (signal.aborted) {
|
|
57
|
+
finalize();
|
|
58
|
+
return Promise.resolve(done<T>());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return new Promise<IteratorResult<T>>((resolve, reject) => {
|
|
62
|
+
// Registered per chunk and released as soon as the chunk settles, so
|
|
63
|
+
// a long stream does not accumulate one reaction per chunk.
|
|
64
|
+
const onAbort = () => {
|
|
65
|
+
finalize();
|
|
66
|
+
resolve(done<T>());
|
|
67
|
+
};
|
|
68
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
69
|
+
const release = () => signal.removeEventListener("abort", onAbort);
|
|
70
|
+
|
|
71
|
+
iterator.next().then(
|
|
72
|
+
(result) => {
|
|
73
|
+
release();
|
|
74
|
+
resolve(result);
|
|
75
|
+
},
|
|
76
|
+
(error: unknown) => {
|
|
77
|
+
release();
|
|
78
|
+
finalize();
|
|
79
|
+
reject(error);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
// The consumer breaks out of the loop to stop a cancelled run, so this is
|
|
85
|
+
// reached on the same path as an abort and finalizes the same way: a
|
|
86
|
+
// source whose own cleanup parks must not stall the break either.
|
|
87
|
+
return: async () => {
|
|
88
|
+
finalize();
|
|
89
|
+
return done<T>();
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -114,6 +114,31 @@ describe("CompositeContextProvider", () => {
|
|
|
114
114
|
expect(composite.getModelContext().system).toBeUndefined();
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("finishes unregistering when the provider cleanup throws", () => {
|
|
118
|
+
const composite = new CompositeContextProvider();
|
|
119
|
+
const cleanupError = new Error("cleanup failed");
|
|
120
|
+
const subscriber = vi.fn();
|
|
121
|
+
const unsubscribe = vi.fn(() => {
|
|
122
|
+
throw cleanupError;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
composite.subscribe(subscriber);
|
|
126
|
+
const unregister = composite.registerModelContextProvider({
|
|
127
|
+
getModelContext: () => ({ system: "provider instructions" }),
|
|
128
|
+
subscribe: () => unsubscribe,
|
|
129
|
+
});
|
|
130
|
+
subscriber.mockClear();
|
|
131
|
+
|
|
132
|
+
expect(() => unregister()).toThrow(cleanupError);
|
|
133
|
+
expect(composite.getModelContext().system).toBeUndefined();
|
|
134
|
+
expect(subscriber).toHaveBeenCalledOnce();
|
|
135
|
+
expect(unsubscribe).toHaveBeenCalledOnce();
|
|
136
|
+
|
|
137
|
+
expect(() => unregister()).not.toThrow();
|
|
138
|
+
expect(subscriber).toHaveBeenCalledOnce();
|
|
139
|
+
expect(unsubscribe).toHaveBeenCalledOnce();
|
|
140
|
+
});
|
|
141
|
+
|
|
117
142
|
it("notifies every subscriber and rethrows on provider updates", () => {
|
|
118
143
|
const composite = new CompositeContextProvider();
|
|
119
144
|
const error = new Error("subscriber failed");
|
|
@@ -32,12 +32,33 @@ export class CompositeContextProvider implements ModelContextProvider {
|
|
|
32
32
|
}
|
|
33
33
|
this._providerUnsubscribes.set(id, unsubscribe);
|
|
34
34
|
this.notifySubscribers();
|
|
35
|
+
let released = false;
|
|
35
36
|
return () => {
|
|
37
|
+
if (released) return;
|
|
38
|
+
released = true;
|
|
36
39
|
this._providers.delete(id);
|
|
37
40
|
const unsubscribe = this._providerUnsubscribes.get(id);
|
|
38
|
-
unsubscribe?.();
|
|
39
41
|
this._providerUnsubscribes.delete(id);
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
let cleanupFailed = false;
|
|
44
|
+
let cleanupError: unknown;
|
|
45
|
+
const runCleanup = (cleanup: () => void) => {
|
|
46
|
+
try {
|
|
47
|
+
cleanup();
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (cleanupFailed) {
|
|
50
|
+
console.error(error);
|
|
51
|
+
} else {
|
|
52
|
+
cleanupFailed = true;
|
|
53
|
+
cleanupError = error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (unsubscribe) runCleanup(unsubscribe);
|
|
59
|
+
runCleanup(() => this.notifySubscribers());
|
|
60
|
+
|
|
61
|
+
if (cleanupFailed) throw cleanupError;
|
|
41
62
|
};
|
|
42
63
|
}
|
|
43
64
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { nullProtoRecord } from "./record";
|
|
3
|
+
|
|
4
|
+
describe("nullProtoRecord", () => {
|
|
5
|
+
it("has no prototype", () => {
|
|
6
|
+
expect(Object.getPrototypeOf(nullProtoRecord())).toBeNull();
|
|
7
|
+
expect(Object.getPrototypeOf(nullProtoRecord({ a: 1 }))).toBeNull();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("copies own entries from every source, later sources winning", () => {
|
|
11
|
+
expect({ ...nullProtoRecord({ a: 1, b: 2 }, undefined, { b: 3 }) }).toEqual(
|
|
12
|
+
{
|
|
13
|
+
a: 1,
|
|
14
|
+
b: 3,
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it.each(["__proto__", "constructor", "toString"])(
|
|
20
|
+
"assigns %s as an own property",
|
|
21
|
+
(key) => {
|
|
22
|
+
const record = nullProtoRecord<number>();
|
|
23
|
+
record[key] = 1;
|
|
24
|
+
|
|
25
|
+
expect(Object.hasOwn(record, key)).toBe(true);
|
|
26
|
+
expect(record[key]).toBe(1);
|
|
27
|
+
expect(Object.keys(record)).toEqual([key]);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
it.each(["__proto__", "constructor", "toString"])(
|
|
32
|
+
"carries %s through a source",
|
|
33
|
+
(key) => {
|
|
34
|
+
const record = nullProtoRecord<number>({ [key]: 1, ok: 2 });
|
|
35
|
+
|
|
36
|
+
expect(Object.keys(record)).toEqual([key, "ok"]);
|
|
37
|
+
expect(record[key]).toBe(1);
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
it("reads an absent prototype-named key as undefined", () => {
|
|
42
|
+
const record = nullProtoRecord<number>({ ok: 1 });
|
|
43
|
+
|
|
44
|
+
expect(record["__proto__"]).toBeUndefined();
|
|
45
|
+
expect(record["constructor"]).toBeUndefined();
|
|
46
|
+
expect(record["toString"]).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a dictionary with no prototype from the own entries of its arguments.
|
|
3
|
+
* Bracket assignment on a plain object routes a `__proto__` key through the
|
|
4
|
+
* `Object.prototype` setter instead of defining an own property, so a record
|
|
5
|
+
* assigned into by a key the app does not control is built this way.
|
|
6
|
+
*/
|
|
7
|
+
export const nullProtoRecord = <T>(
|
|
8
|
+
base?: Readonly<Record<string, T>>,
|
|
9
|
+
...rest: readonly (Readonly<Record<string, NoInfer<T>>> | undefined)[]
|
|
10
|
+
): Record<string, T> =>
|
|
11
|
+
Object.assign(Object.create(null) as Record<string, T>, base, ...rest);
|