@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
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
classifyThreads,
|
|
4
4
|
createEmptyRemoteThreadState,
|
|
5
5
|
createThreadMappingId,
|
|
6
|
+
getThreadData,
|
|
7
|
+
promoteNewThreadReducer,
|
|
8
|
+
reconcileInitializedThread,
|
|
6
9
|
seedNewThread,
|
|
7
10
|
updateStatusReducer,
|
|
8
11
|
} from "./remote-thread-state";
|
|
@@ -13,12 +16,16 @@ import type {
|
|
|
13
16
|
|
|
14
17
|
const initializedDraft = () => {
|
|
15
18
|
const seeded = seedNewThread(createEmptyRemoteThreadState());
|
|
16
|
-
const regular = updateStatusReducer(seeded.state, seeded.id, "regular");
|
|
17
|
-
const mappingId = regular.threadIdMap[seeded.id]!;
|
|
18
19
|
const initializeTask = Promise.resolve({
|
|
19
20
|
remoteId: "remote-1",
|
|
20
21
|
externalId: "remote-1",
|
|
21
22
|
});
|
|
23
|
+
const regular = promoteNewThreadReducer(
|
|
24
|
+
seeded.state,
|
|
25
|
+
seeded.id,
|
|
26
|
+
initializeTask,
|
|
27
|
+
);
|
|
28
|
+
const mappingId = regular.threadIdMap[seeded.id]!;
|
|
22
29
|
return {
|
|
23
30
|
id: seeded.id,
|
|
24
31
|
mappingId,
|
|
@@ -54,6 +61,7 @@ describe("remote thread state", () => {
|
|
|
54
61
|
it("creates an empty state", () => {
|
|
55
62
|
expect(createEmptyRemoteThreadState()).toEqual({
|
|
56
63
|
isLoading: true,
|
|
64
|
+
loadError: undefined,
|
|
57
65
|
isLoadingMore: false,
|
|
58
66
|
cursor: undefined,
|
|
59
67
|
newThreadId: undefined,
|
|
@@ -80,6 +88,39 @@ describe("remote thread state", () => {
|
|
|
80
88
|
expect(Object.keys(second.state.threadData)).toEqual([first.id, second.id]);
|
|
81
89
|
});
|
|
82
90
|
|
|
91
|
+
it("carries the initialization task onto the promoted slot", () => {
|
|
92
|
+
const seeded = seedNewThread(createEmptyRemoteThreadState());
|
|
93
|
+
const initializeTask = Promise.resolve({
|
|
94
|
+
remoteId: "remote-1",
|
|
95
|
+
externalId: "external-1",
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const promoted = promoteNewThreadReducer(
|
|
99
|
+
seeded.state,
|
|
100
|
+
seeded.id,
|
|
101
|
+
initializeTask,
|
|
102
|
+
);
|
|
103
|
+
const data = getThreadData(promoted, seeded.id);
|
|
104
|
+
|
|
105
|
+
expect(data?.status).toBe("regular");
|
|
106
|
+
expect(data?.status === "new" ? undefined : data?.initializeTask).toBe(
|
|
107
|
+
initializeTask,
|
|
108
|
+
);
|
|
109
|
+
expect(promoted.newThreadId).toBeUndefined();
|
|
110
|
+
expect(promoted.threadIds).toEqual([seeded.id]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it.each(["regular", "archived"] as const)(
|
|
114
|
+
"leaves a new thread untouched when moved to %s without an initialization task",
|
|
115
|
+
(newStatus) => {
|
|
116
|
+
const seeded = seedNewThread(createEmptyRemoteThreadState());
|
|
117
|
+
|
|
118
|
+
expect(updateStatusReducer(seeded.state, seeded.id, newStatus)).toBe(
|
|
119
|
+
seeded.state,
|
|
120
|
+
);
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
83
124
|
it("refreshes the local slot when a listed thread already has a mapping", () => {
|
|
84
125
|
const draft = initializedDraft();
|
|
85
126
|
|
|
@@ -131,6 +172,91 @@ describe("remote thread state", () => {
|
|
|
131
172
|
expect(listed.threadData[createThreadMappingId("a")]?.title).toBe("second");
|
|
132
173
|
});
|
|
133
174
|
|
|
175
|
+
it("retains a listed external id when that slot survives reconciliation", async () => {
|
|
176
|
+
const seeded = seedNewThread(createEmptyRemoteThreadState());
|
|
177
|
+
const regular = promoteNewThreadReducer(
|
|
178
|
+
seeded.state,
|
|
179
|
+
seeded.id,
|
|
180
|
+
Promise.resolve({ remoteId: "remote-1", externalId: "external-1" }),
|
|
181
|
+
);
|
|
182
|
+
const classified = classifyThreads(
|
|
183
|
+
[
|
|
184
|
+
{
|
|
185
|
+
status: "regular",
|
|
186
|
+
remoteId: "remote-1",
|
|
187
|
+
externalId: "external-1",
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
{
|
|
191
|
+
threadIds: [...regular.threadIds],
|
|
192
|
+
archivedThreadIds: [...regular.archivedThreadIds],
|
|
193
|
+
threadIdMap: { ...regular.threadIdMap },
|
|
194
|
+
threadData: { ...regular.threadData },
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
const state = { ...regular, ...classified };
|
|
198
|
+
|
|
199
|
+
const reconciled = reconcileInitializedThread(
|
|
200
|
+
state,
|
|
201
|
+
seeded.id,
|
|
202
|
+
"remote-1",
|
|
203
|
+
undefined,
|
|
204
|
+
"remote-1",
|
|
205
|
+
);
|
|
206
|
+
const survivor = reconciled.state.threadData[reconciled.survivorMappingId]!;
|
|
207
|
+
|
|
208
|
+
expect(survivor.externalId).toBe("external-1");
|
|
209
|
+
if (survivor.status === "new") throw new Error("Expected initialized slot");
|
|
210
|
+
await expect(survivor.initializeTask).resolves.toEqual({
|
|
211
|
+
remoteId: "remote-1",
|
|
212
|
+
externalId: "external-1",
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it.each(["__proto__", "constructor", "toString"])(
|
|
217
|
+
"handles a prototype-named remote id %s",
|
|
218
|
+
(remoteId) => {
|
|
219
|
+
const classified = classifyThreads(
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
status: "regular",
|
|
223
|
+
remoteId,
|
|
224
|
+
externalId: undefined,
|
|
225
|
+
title: `title-${remoteId}`,
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
status: "regular",
|
|
229
|
+
remoteId: "ok",
|
|
230
|
+
externalId: undefined,
|
|
231
|
+
title: "title-ok",
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
{
|
|
235
|
+
threadIds: [],
|
|
236
|
+
archivedThreadIds: [],
|
|
237
|
+
threadIdMap: {},
|
|
238
|
+
threadData: {},
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
const listed: RemoteThreadState = {
|
|
242
|
+
...createEmptyRemoteThreadState(),
|
|
243
|
+
threadIds: classified.threadIds,
|
|
244
|
+
archivedThreadIds: classified.archivedThreadIds,
|
|
245
|
+
threadIdMap: classified.threadIdMap,
|
|
246
|
+
threadData: classified.threadData,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
expect(Object.keys(listed.threadIdMap)).toEqual([remoteId, "ok"]);
|
|
250
|
+
expect(typeof listed.threadIdMap[remoteId]).toBe("string");
|
|
251
|
+
expect(Object.keys(listed.threadData)).toEqual([remoteId, "ok"]);
|
|
252
|
+
|
|
253
|
+
const deleted = updateStatusReducer(listed, remoteId, "deleted");
|
|
254
|
+
expect(Object.keys(deleted.threadIdMap)).toEqual(["ok"]);
|
|
255
|
+
expect(Object.keys(deleted.threadData)).toEqual(["ok"]);
|
|
256
|
+
expect(deleted.threadIds).toEqual(["ok"]);
|
|
257
|
+
},
|
|
258
|
+
);
|
|
259
|
+
|
|
134
260
|
it("deletes every alias of an identity, by either id", () => {
|
|
135
261
|
for (const target of ["remote-1", "local"] as const) {
|
|
136
262
|
const draft = initializedDraft();
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
RemoteThreadMetadata,
|
|
4
4
|
} from "./types";
|
|
5
5
|
import { generateId } from "../../utils/id";
|
|
6
|
+
import { nullProtoRecord } from "../../utils/record";
|
|
6
7
|
|
|
7
8
|
export type RemoteThreadData =
|
|
8
9
|
| {
|
|
@@ -64,7 +65,11 @@ export const classifyThreads = (
|
|
|
64
65
|
threads: readonly RemoteThreadMetadata[],
|
|
65
66
|
acc: ClassifyAccumulator,
|
|
66
67
|
): ClassifyAccumulator => {
|
|
67
|
-
|
|
68
|
+
let threadIds = [...acc.threadIds];
|
|
69
|
+
let archivedThreadIds = [...acc.archivedThreadIds];
|
|
70
|
+
const threadIdMap = nullProtoRecord(acc.threadIdMap);
|
|
71
|
+
const threadData = nullProtoRecord(acc.threadData);
|
|
72
|
+
const listed = new Set([...threadIds, ...archivedThreadIds]);
|
|
68
73
|
|
|
69
74
|
for (const thread of threads) {
|
|
70
75
|
switch (thread.status) {
|
|
@@ -77,10 +82,10 @@ export const classifyThreads = (
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
const existingMappingId =
|
|
85
|
+
const existingMappingId = threadIdMap[thread.remoteId];
|
|
81
86
|
const existing =
|
|
82
87
|
existingMappingId !== undefined
|
|
83
|
-
?
|
|
88
|
+
? threadData[existingMappingId]
|
|
84
89
|
: undefined;
|
|
85
90
|
const id = existing?.id ?? thread.remoteId;
|
|
86
91
|
const mappingId = existingMappingId ?? createThreadMappingId(id);
|
|
@@ -92,23 +97,23 @@ export const classifyThreads = (
|
|
|
92
97
|
if (!listed.has(id)) {
|
|
93
98
|
listed.add(id);
|
|
94
99
|
if (thread.status === "regular") {
|
|
95
|
-
|
|
100
|
+
threadIds.push(id);
|
|
96
101
|
} else {
|
|
97
|
-
|
|
102
|
+
archivedThreadIds.push(id);
|
|
98
103
|
}
|
|
99
104
|
} else if (existing !== undefined && existing.status !== thread.status) {
|
|
100
105
|
if (thread.status === "regular") {
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
archivedThreadIds = archivedThreadIds.filter((t) => t !== id);
|
|
107
|
+
threadIds.push(id);
|
|
103
108
|
} else {
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
threadIds = threadIds.filter((t) => t !== id);
|
|
110
|
+
archivedThreadIds.push(id);
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
threadIdMap[id] = mappingId;
|
|
115
|
+
threadIdMap[thread.remoteId] = mappingId;
|
|
116
|
+
threadData[mappingId] = {
|
|
112
117
|
...(existing?.localOrigin === true ? { localOrigin: true as const } : {}),
|
|
113
118
|
id,
|
|
114
119
|
remoteId: thread.remoteId,
|
|
@@ -125,11 +130,12 @@ export const classifyThreads = (
|
|
|
125
130
|
}),
|
|
126
131
|
};
|
|
127
132
|
}
|
|
128
|
-
return
|
|
133
|
+
return { threadIds, archivedThreadIds, threadIdMap, threadData };
|
|
129
134
|
};
|
|
130
135
|
|
|
131
136
|
export type RemoteThreadState = {
|
|
132
137
|
readonly isLoading: boolean;
|
|
138
|
+
readonly loadError: unknown;
|
|
133
139
|
readonly isLoadingMore: boolean;
|
|
134
140
|
readonly cursor: string | undefined;
|
|
135
141
|
readonly newThreadId: string | undefined;
|
|
@@ -141,13 +147,14 @@ export type RemoteThreadState = {
|
|
|
141
147
|
|
|
142
148
|
export const createEmptyRemoteThreadState = (): RemoteThreadState => ({
|
|
143
149
|
isLoading: true,
|
|
150
|
+
loadError: undefined,
|
|
144
151
|
isLoadingMore: false,
|
|
145
152
|
cursor: undefined,
|
|
146
153
|
newThreadId: undefined,
|
|
147
154
|
threadIds: [],
|
|
148
155
|
archivedThreadIds: [],
|
|
149
|
-
threadIdMap:
|
|
150
|
-
threadData:
|
|
156
|
+
threadIdMap: nullProtoRecord(),
|
|
157
|
+
threadData: nullProtoRecord(),
|
|
151
158
|
});
|
|
152
159
|
|
|
153
160
|
export const seedNewThread = (
|
|
@@ -163,12 +170,10 @@ export const seedNewThread = (
|
|
|
163
170
|
state: {
|
|
164
171
|
...state,
|
|
165
172
|
newThreadId: id,
|
|
166
|
-
threadIdMap: {
|
|
167
|
-
...state.threadIdMap,
|
|
173
|
+
threadIdMap: nullProtoRecord(state.threadIdMap, {
|
|
168
174
|
[id]: mappingId,
|
|
169
|
-
},
|
|
170
|
-
threadData: {
|
|
171
|
-
...state.threadData,
|
|
175
|
+
}),
|
|
176
|
+
threadData: nullProtoRecord(state.threadData, {
|
|
172
177
|
[mappingId]: {
|
|
173
178
|
status: "new",
|
|
174
179
|
id,
|
|
@@ -178,7 +183,7 @@ export const seedNewThread = (
|
|
|
178
183
|
custom: undefined,
|
|
179
184
|
localOrigin: true,
|
|
180
185
|
} satisfies RemoteThreadData,
|
|
181
|
-
},
|
|
186
|
+
}),
|
|
182
187
|
},
|
|
183
188
|
};
|
|
184
189
|
};
|
|
@@ -260,15 +265,124 @@ export const getThreadData = (
|
|
|
260
265
|
state: RemoteThreadState,
|
|
261
266
|
threadIdOrRemoteId: string,
|
|
262
267
|
) => {
|
|
263
|
-
const idx = state.threadIdMap
|
|
268
|
+
const idx = Object.hasOwn(state.threadIdMap, threadIdOrRemoteId)
|
|
269
|
+
? state.threadIdMap[threadIdOrRemoteId]
|
|
270
|
+
: undefined;
|
|
264
271
|
if (idx === undefined) return undefined;
|
|
265
|
-
return state.threadData
|
|
272
|
+
return Object.hasOwn(state.threadData, idx)
|
|
273
|
+
? state.threadData[idx]
|
|
274
|
+
: undefined;
|
|
266
275
|
};
|
|
267
276
|
|
|
268
|
-
export const
|
|
277
|
+
export const reconcileInitializedThread = (
|
|
278
|
+
state: RemoteThreadState,
|
|
279
|
+
threadId: string,
|
|
280
|
+
remoteId: string,
|
|
281
|
+
externalId: string | undefined,
|
|
282
|
+
retainedThreadId?: string,
|
|
283
|
+
): {
|
|
284
|
+
state: RemoteThreadState;
|
|
285
|
+
removedMappingId: THREAD_MAPPING_ID | undefined;
|
|
286
|
+
survivorMappingId: THREAD_MAPPING_ID;
|
|
287
|
+
} => {
|
|
288
|
+
const mappingId = createThreadMappingId(threadId);
|
|
289
|
+
const data = Object.hasOwn(state.threadData, mappingId)
|
|
290
|
+
? state.threadData[mappingId]
|
|
291
|
+
: undefined;
|
|
292
|
+
if (!data) {
|
|
293
|
+
return { state, removedMappingId: undefined, survivorMappingId: mappingId };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// A concurrent list cannot associate the remote ID with this initializing
|
|
297
|
+
// slot and may mint a duplicate. Retain the slot whose mounted runtime owns
|
|
298
|
+
// local state; initialized metadata remains authoritative over list metadata.
|
|
299
|
+
const listedMappingId = Object.hasOwn(state.threadIdMap, remoteId)
|
|
300
|
+
? state.threadIdMap[remoteId]
|
|
301
|
+
: undefined;
|
|
302
|
+
const listedData =
|
|
303
|
+
listedMappingId !== undefined &&
|
|
304
|
+
listedMappingId !== mappingId &&
|
|
305
|
+
Object.hasOwn(state.threadData, listedMappingId)
|
|
306
|
+
? state.threadData[listedMappingId]
|
|
307
|
+
: undefined;
|
|
308
|
+
const listedSlot =
|
|
309
|
+
listedMappingId !== undefined && listedData !== undefined
|
|
310
|
+
? { mappingId: listedMappingId, data: listedData }
|
|
311
|
+
: undefined;
|
|
312
|
+
const retainedMappingId =
|
|
313
|
+
retainedThreadId !== undefined &&
|
|
314
|
+
Object.hasOwn(state.threadIdMap, retainedThreadId)
|
|
315
|
+
? state.threadIdMap[retainedThreadId]
|
|
316
|
+
: undefined;
|
|
317
|
+
const survivorMappingId =
|
|
318
|
+
listedSlot !== undefined && retainedMappingId === listedSlot.mappingId
|
|
319
|
+
? listedSlot.mappingId
|
|
320
|
+
: mappingId;
|
|
321
|
+
const removedMappingId =
|
|
322
|
+
listedSlot === undefined
|
|
323
|
+
? undefined
|
|
324
|
+
: survivorMappingId === mappingId
|
|
325
|
+
? listedSlot.mappingId
|
|
326
|
+
: mappingId;
|
|
327
|
+
const resolvedExternalId = externalId ?? listedSlot?.data.externalId;
|
|
328
|
+
const threadData = nullProtoRecord(state.threadData);
|
|
329
|
+
if (removedMappingId !== undefined) delete threadData[removedMappingId];
|
|
330
|
+
const initializedData = {
|
|
331
|
+
...data,
|
|
332
|
+
id: survivorMappingId,
|
|
333
|
+
initializeTask: Promise.resolve({
|
|
334
|
+
remoteId,
|
|
335
|
+
externalId: resolvedExternalId,
|
|
336
|
+
}),
|
|
337
|
+
remoteId,
|
|
338
|
+
externalId: resolvedExternalId,
|
|
339
|
+
} as RemoteThreadData;
|
|
340
|
+
threadData[survivorMappingId] = {
|
|
341
|
+
...listedSlot?.data,
|
|
342
|
+
...initializedData,
|
|
343
|
+
title: data.title ?? listedSlot?.data.title,
|
|
344
|
+
lastMessageAt:
|
|
345
|
+
("lastMessageAt" in data ? data.lastMessageAt : undefined) ??
|
|
346
|
+
(listedSlot && "lastMessageAt" in listedSlot.data
|
|
347
|
+
? listedSlot.data.lastMessageAt
|
|
348
|
+
: undefined),
|
|
349
|
+
custom: data.custom ?? listedSlot?.data.custom,
|
|
350
|
+
} as RemoteThreadData;
|
|
351
|
+
|
|
352
|
+
const threadIdMap = nullProtoRecord(state.threadIdMap);
|
|
353
|
+
if (removedMappingId !== undefined) {
|
|
354
|
+
for (const [id, target] of Object.entries(threadIdMap)) {
|
|
355
|
+
if (target === removedMappingId) threadIdMap[id] = survivorMappingId;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
threadIdMap[threadId] = survivorMappingId;
|
|
359
|
+
threadIdMap[remoteId] = survivorMappingId;
|
|
360
|
+
|
|
361
|
+
const rewire = (ids: readonly string[]) =>
|
|
362
|
+
listedSlot === undefined
|
|
363
|
+
? ids
|
|
364
|
+
: ids
|
|
365
|
+
.filter((id) => id !== listedSlot.data.id)
|
|
366
|
+
.map((id) => (id === data.id ? survivorMappingId : id));
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
state: {
|
|
370
|
+
...state,
|
|
371
|
+
threadIds: rewire(state.threadIds),
|
|
372
|
+
archivedThreadIds: rewire(state.archivedThreadIds),
|
|
373
|
+
threadIdMap,
|
|
374
|
+
threadData,
|
|
375
|
+
},
|
|
376
|
+
removedMappingId,
|
|
377
|
+
survivorMappingId,
|
|
378
|
+
};
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const transitionReducer = (
|
|
269
382
|
state: RemoteThreadState,
|
|
270
383
|
threadIdOrRemoteId: string,
|
|
271
384
|
newStatus: "regular" | "archived" | "deleted",
|
|
385
|
+
initializeTask: Promise<RemoteThreadInitializeResponse> | undefined,
|
|
272
386
|
) => {
|
|
273
387
|
const data = getThreadData(state, threadIdOrRemoteId);
|
|
274
388
|
if (!data) return state;
|
|
@@ -276,6 +390,18 @@ export const updateStatusReducer = (
|
|
|
276
390
|
const { id, status: lastStatus } = data;
|
|
277
391
|
if (lastStatus === newStatus) return state;
|
|
278
392
|
|
|
393
|
+
let nextData: RemoteThreadData | undefined;
|
|
394
|
+
if (newStatus !== "deleted") {
|
|
395
|
+
if (data.status === "new") {
|
|
396
|
+
// The new variant holds no initialization promise, so the destination
|
|
397
|
+
// union member cannot be built unless the caller supplies one.
|
|
398
|
+
if (initializeTask === undefined) return state;
|
|
399
|
+
nextData = { ...data, initializeTask, status: newStatus };
|
|
400
|
+
} else {
|
|
401
|
+
nextData = { ...data, status: newStatus };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
279
405
|
const newState = { ...state };
|
|
280
406
|
|
|
281
407
|
// lastStatus
|
|
@@ -310,16 +436,14 @@ export const updateStatusReducer = (
|
|
|
310
436
|
|
|
311
437
|
case "deleted": {
|
|
312
438
|
const mappingId = state.threadIdMap[threadIdOrRemoteId]!;
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
),
|
|
322
|
-
);
|
|
439
|
+
const threadData = nullProtoRecord(newState.threadData);
|
|
440
|
+
delete threadData[mappingId];
|
|
441
|
+
newState.threadData = threadData;
|
|
442
|
+
const threadIdMap = nullProtoRecord(newState.threadIdMap);
|
|
443
|
+
for (const [key, value] of Object.entries(threadIdMap)) {
|
|
444
|
+
if (value === mappingId) delete threadIdMap[key];
|
|
445
|
+
}
|
|
446
|
+
newState.threadIdMap = threadIdMap;
|
|
323
447
|
break;
|
|
324
448
|
}
|
|
325
449
|
|
|
@@ -329,15 +453,31 @@ export const updateStatusReducer = (
|
|
|
329
453
|
}
|
|
330
454
|
}
|
|
331
455
|
|
|
332
|
-
if (
|
|
333
|
-
newState.threadData = {
|
|
334
|
-
...newState.threadData,
|
|
335
|
-
[id]: {
|
|
336
|
-
...data,
|
|
337
|
-
status: newStatus,
|
|
338
|
-
},
|
|
339
|
-
};
|
|
456
|
+
if (nextData !== undefined) {
|
|
457
|
+
newState.threadData = { ...newState.threadData, [id]: nextData };
|
|
340
458
|
}
|
|
341
459
|
|
|
342
460
|
return newState;
|
|
343
461
|
};
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Transitions a thread that already exists remotely. Promoting the local draft
|
|
465
|
+
* off `new` needs the initialization promise its destination variant requires,
|
|
466
|
+
* so that goes through `promoteNewThreadReducer`; asking for it here leaves the
|
|
467
|
+
* state unchanged.
|
|
468
|
+
*/
|
|
469
|
+
export const updateStatusReducer = (
|
|
470
|
+
state: RemoteThreadState,
|
|
471
|
+
threadIdOrRemoteId: string,
|
|
472
|
+
newStatus: "regular" | "archived" | "deleted",
|
|
473
|
+
) => transitionReducer(state, threadIdOrRemoteId, newStatus, undefined);
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Promotes the local draft to `regular`, carrying the initialization promise
|
|
477
|
+
* onto the slot.
|
|
478
|
+
*/
|
|
479
|
+
export const promoteNewThreadReducer = (
|
|
480
|
+
state: RemoteThreadState,
|
|
481
|
+
threadIdOrRemoteId: string,
|
|
482
|
+
initializeTask: Promise<RemoteThreadInitializeResponse>,
|
|
483
|
+
) => transitionReducer(state, threadIdOrRemoteId, "regular", initializeTask);
|