@assistant-ui/core 0.3.14 → 0.3.16

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.
Files changed (341) hide show
  1. package/dist/adapters/voice.js +11 -0
  2. package/dist/adapters/voice.js.map +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/internal.d.ts +8 -2
  5. package/dist/internal.js +9 -3
  6. package/dist/model-context/frame/host.d.ts +1 -1
  7. package/dist/model-context/frame/host.d.ts.map +1 -1
  8. package/dist/model-context/frame/host.js +20 -5
  9. package/dist/model-context/frame/host.js.map +1 -1
  10. package/dist/model-context/frame/provider.d.ts +6 -0
  11. package/dist/model-context/frame/provider.d.ts.map +1 -1
  12. package/dist/model-context/frame/provider.js +126 -16
  13. package/dist/model-context/frame/provider.js.map +1 -1
  14. package/dist/model-context/frame/types.d.ts +4 -1
  15. package/dist/model-context/frame/types.d.ts.map +1 -1
  16. package/dist/model-context/frame/types.js.map +1 -1
  17. package/dist/model-context/interactable-composer-metadata.d.ts +1 -1
  18. package/dist/model-context/interactable-composer-metadata.js +1 -1
  19. package/dist/model-context/interactable-composer-metadata.js.map +1 -1
  20. package/dist/model-context/registry.d.ts +2 -2
  21. package/dist/model-context/registry.d.ts.map +1 -1
  22. package/dist/model-context/registry.js +9 -28
  23. package/dist/model-context/registry.js.map +1 -1
  24. package/dist/react/client/InMemoryThreadList.d.ts.map +1 -1
  25. package/dist/react/client/InMemoryThreadList.js +24 -20
  26. package/dist/react/client/InMemoryThreadList.js.map +1 -1
  27. package/dist/react/client/Interactables.d.ts.map +1 -1
  28. package/dist/react/client/Interactables.js +59 -177
  29. package/dist/react/client/Interactables.js.map +1 -1
  30. package/dist/react/client/RemoteThreadList.d.ts +1 -1
  31. package/dist/react/client/RemoteThreadList.d.ts.map +1 -1
  32. package/dist/react/client/RemoteThreadList.js +117 -78
  33. package/dist/react/client/RemoteThreadList.js.map +1 -1
  34. package/dist/react/index.d.ts +2 -1
  35. package/dist/react/interactables-legacy/Interactables.d.ts.map +1 -1
  36. package/dist/react/interactables-legacy/Interactables.js +56 -174
  37. package/dist/react/interactables-legacy/Interactables.js.map +1 -1
  38. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts +24 -0
  39. package/dist/react/interactables-shared/useInteractablePersistenceQueue.d.ts.map +1 -0
  40. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js +138 -0
  41. package/dist/react/interactables-shared/useInteractablePersistenceQueue.js.map +1 -0
  42. package/dist/react/primitive-hooks/useActionBarCopy.js +7 -1
  43. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -1
  44. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +3 -0
  45. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -1
  46. package/dist/react/primitive-hooks/useThreadIsEmpty.js +3 -0
  47. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -1
  48. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +3 -0
  49. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -1
  50. package/dist/react/primitive-hooks/useThreadIsRunning.js +3 -0
  51. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -1
  52. package/dist/react/primitives/composer/ComposerIf.js.map +1 -1
  53. package/dist/react/primitives/composer/ComposerQueue.d.ts +2 -1
  54. package/dist/react/primitives/composer/ComposerQueue.d.ts.map +1 -1
  55. package/dist/react/primitives/message/MessageParts.js.map +1 -1
  56. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts +3 -1
  57. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.d.ts.map +1 -1
  58. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js +6 -0
  59. package/dist/react/runtimes/RemoteThreadListHookInstanceManager.js.map +1 -1
  60. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts +7 -1
  61. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.d.ts.map +1 -1
  62. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js +174 -64
  63. package/dist/react/runtimes/RemoteThreadListThreadListRuntimeCore.js.map +1 -1
  64. package/dist/react/runtimes/RemoteThreadResource.d.ts.map +1 -1
  65. package/dist/react/runtimes/RemoteThreadResource.js +2 -0
  66. package/dist/react/runtimes/RemoteThreadResource.js.map +1 -1
  67. package/dist/react/runtimes/assistant-transport/runManager.d.ts.map +1 -1
  68. package/dist/react/runtimes/assistant-transport/runManager.js +2 -12
  69. package/dist/react/runtimes/assistant-transport/runManager.js.map +1 -1
  70. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
  71. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js +1 -1
  72. package/dist/react/runtimes/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
  73. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts +3 -10
  74. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.d.ts.map +1 -1
  75. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js +20 -67
  76. package/dist/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.js.map +1 -1
  77. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts +12 -0
  78. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.d.ts.map +1 -0
  79. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js +10 -0
  80. package/dist/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.js.map +1 -0
  81. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  82. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +38 -5
  83. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  84. package/dist/react/runtimes/cloud/useCloudThreadListRuntime.js.map +1 -1
  85. package/dist/react/runtimes/createMessageConverter.d.ts +2 -1
  86. package/dist/react/runtimes/createMessageConverter.d.ts.map +1 -1
  87. package/dist/react/runtimes/external-message-converter.d.ts +6 -25
  88. package/dist/react/runtimes/external-message-converter.d.ts.map +1 -1
  89. package/dist/react/runtimes/external-message-converter.js +33 -233
  90. package/dist/react/runtimes/external-message-converter.js.map +1 -1
  91. package/dist/runtime/api/composer-runtime.d.ts +1 -1
  92. package/dist/runtime/api/composer-runtime.js.map +1 -1
  93. package/dist/runtime/api/thread-runtime.d.ts +1 -2
  94. package/dist/runtime/api/thread-runtime.d.ts.map +1 -1
  95. package/dist/runtime/base/base-composer-runtime-core.d.ts +1 -1
  96. package/dist/runtime/base/base-composer-runtime-core.d.ts.map +1 -1
  97. package/dist/runtime/base/base-composer-runtime-core.js +14 -29
  98. package/dist/runtime/base/base-composer-runtime-core.js.map +1 -1
  99. package/dist/runtime/base/base-thread-runtime-core.d.ts +2 -4
  100. package/dist/runtime/base/base-thread-runtime-core.d.ts.map +1 -1
  101. package/dist/runtime/base/base-thread-runtime-core.js +3 -9
  102. package/dist/runtime/base/base-thread-runtime-core.js.map +1 -1
  103. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts +1 -1
  104. package/dist/runtime/base/default-thread-composer-runtime-core.d.ts.map +1 -1
  105. package/dist/runtime/base/default-thread-composer-runtime-core.js +1 -1
  106. package/dist/runtime/base/default-thread-composer-runtime-core.js.map +1 -1
  107. package/dist/runtime/interfaces/composer-runtime-core.d.ts +1 -1
  108. package/dist/runtime/interfaces/thread-runtime-core.d.ts +1 -1
  109. package/dist/runtime/internal.d.ts +2 -1
  110. package/dist/runtime/internal.js +2 -1
  111. package/dist/runtime/queue/external-thread-queue-adapter.d.ts +1 -1
  112. package/dist/runtime/queue/message-queue.d.ts.map +1 -1
  113. package/dist/runtime/queue/message-queue.js +1 -1
  114. package/dist/runtime/queue/message-queue.js.map +1 -1
  115. package/dist/runtime/queue/queue-item.d.ts +12 -0
  116. package/dist/runtime/queue/queue-item.d.ts.map +1 -0
  117. package/dist/runtime/queue/queue-item.js +6 -0
  118. package/dist/runtime/queue/queue-item.js.map +1 -0
  119. package/dist/runtime/utils/abortable-thread-load.d.ts +15 -0
  120. package/dist/runtime/utils/abortable-thread-load.d.ts.map +1 -0
  121. package/dist/runtime/utils/abortable-thread-load.js +35 -0
  122. package/dist/runtime/utils/abortable-thread-load.js.map +1 -0
  123. package/dist/runtime/utils/attachment-add-operations.d.ts +18 -0
  124. package/dist/runtime/utils/attachment-add-operations.d.ts.map +1 -0
  125. package/dist/runtime/utils/attachment-add-operations.js +43 -0
  126. package/dist/runtime/utils/attachment-add-operations.js.map +1 -0
  127. package/dist/runtime/utils/auto-status.d.ts +6 -1
  128. package/dist/runtime/utils/auto-status.d.ts.map +1 -1
  129. package/dist/runtime/utils/auto-status.js +7 -1
  130. package/dist/runtime/utils/auto-status.js.map +1 -1
  131. package/dist/runtime/utils/external-message-conversion.d.ts +47 -0
  132. package/dist/runtime/utils/external-message-conversion.d.ts.map +1 -0
  133. package/dist/runtime/utils/external-message-conversion.js +221 -0
  134. package/dist/runtime/utils/external-message-conversion.js.map +1 -0
  135. package/dist/runtime/utils/external-store-message.d.ts +2 -1
  136. package/dist/runtime/utils/external-store-message.d.ts.map +1 -1
  137. package/dist/runtime/utils/external-store-message.js +2 -1
  138. package/dist/runtime/utils/external-store-message.js.map +1 -1
  139. package/dist/runtime/utils/message-repository-session.d.ts +30 -0
  140. package/dist/runtime/utils/message-repository-session.d.ts.map +1 -0
  141. package/dist/runtime/utils/message-repository-session.js +130 -0
  142. package/dist/runtime/utils/message-repository-session.js.map +1 -0
  143. package/dist/runtime/utils/message-repository.d.ts.map +1 -1
  144. package/dist/runtime/utils/message-repository.js +30 -22
  145. package/dist/runtime/utils/message-repository.js.map +1 -1
  146. package/dist/runtime/utils/pending-tool-calls.d.ts +20 -0
  147. package/dist/runtime/utils/pending-tool-calls.d.ts.map +1 -0
  148. package/dist/runtime/utils/pending-tool-calls.js +22 -0
  149. package/dist/runtime/utils/pending-tool-calls.js.map +1 -0
  150. package/dist/runtime/utils/thread-message-like.d.ts +2 -1
  151. package/dist/runtime/utils/thread-message-like.d.ts.map +1 -1
  152. package/dist/runtime/utils/thread-message-like.js.map +1 -1
  153. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts +2 -5
  154. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.d.ts.map +1 -1
  155. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js +3 -10
  156. package/dist/runtimes/external-store/external-store-thread-list-runtime-core.js.map +1 -1
  157. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts +19 -1
  158. package/dist/runtimes/external-store/external-store-thread-runtime-core.d.ts.map +1 -1
  159. package/dist/runtimes/external-store/external-store-thread-runtime-core.js +124 -20
  160. package/dist/runtimes/external-store/external-store-thread-runtime-core.js.map +1 -1
  161. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts +102 -0
  162. package/dist/runtimes/inert/InertThreadRuntimeCore.d.ts.map +1 -0
  163. package/dist/runtimes/inert/InertThreadRuntimeCore.js +156 -0
  164. package/dist/runtimes/inert/InertThreadRuntimeCore.js.map +1 -0
  165. package/dist/runtimes/local/local-thread-runtime-core.d.ts +3 -1
  166. package/dist/runtimes/local/local-thread-runtime-core.d.ts.map +1 -1
  167. package/dist/runtimes/local/local-thread-runtime-core.js +45 -8
  168. package/dist/runtimes/local/local-thread-runtime-core.js.map +1 -1
  169. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts +12 -60
  170. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.d.ts.map +1 -1
  171. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js +10 -145
  172. package/dist/runtimes/readonly/ReadonlyThreadRuntimeCore.js.map +1 -1
  173. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts +9 -0
  174. package/dist/runtimes/remote-thread-list/adapter-changed.d.ts.map +1 -0
  175. package/dist/runtimes/remote-thread-list/adapter-changed.js +13 -0
  176. package/dist/runtimes/remote-thread-list/adapter-changed.js.map +1 -0
  177. package/dist/runtimes/remote-thread-list/empty-thread-core.d.ts.map +1 -1
  178. package/dist/runtimes/remote-thread-list/empty-thread-core.js +16 -155
  179. package/dist/runtimes/remote-thread-list/empty-thread-core.js.map +1 -1
  180. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts +2 -0
  181. package/dist/runtimes/remote-thread-list/optimistic-state.d.ts.map +1 -1
  182. package/dist/runtimes/remote-thread-list/optimistic-state.js +11 -0
  183. package/dist/runtimes/remote-thread-list/optimistic-state.js.map +1 -1
  184. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts +12 -1
  185. package/dist/runtimes/remote-thread-list/remote-thread-state.d.ts.map +1 -1
  186. package/dist/runtimes/remote-thread-list/remote-thread-state.js +77 -1
  187. package/dist/runtimes/remote-thread-list/remote-thread-state.js.map +1 -1
  188. package/dist/runtimes/remote-thread-list/types.d.ts +3 -0
  189. package/dist/runtimes/remote-thread-list/types.d.ts.map +1 -1
  190. package/dist/runtimes/tool-invocations/ToolInvocationTracker.d.ts.map +1 -1
  191. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js +2 -0
  192. package/dist/runtimes/tool-invocations/ToolInvocationTracker.js.map +1 -1
  193. package/dist/store/clients/external-thread.d.ts.map +1 -1
  194. package/dist/store/clients/external-thread.js +75 -100
  195. package/dist/store/clients/external-thread.js.map +1 -1
  196. package/dist/store/clients/single-thread-list.d.ts.map +1 -1
  197. package/dist/store/clients/single-thread-list.js +1 -1
  198. package/dist/store/clients/single-thread-list.js.map +1 -1
  199. package/dist/store/clients/suggestions.d.ts.map +1 -1
  200. package/dist/store/clients/suggestions.js +16 -7
  201. package/dist/store/clients/suggestions.js.map +1 -1
  202. package/dist/store/index.d.ts +4 -2
  203. package/dist/store/runtime-clients/handle-runtime-action.d.ts.map +1 -1
  204. package/dist/store/runtime-clients/handle-runtime-action.js +2 -0
  205. package/dist/store/runtime-clients/handle-runtime-action.js.map +1 -1
  206. package/dist/store/scope-registration.d.ts +1 -1
  207. package/dist/store/scopes/composer.d.ts +2 -1
  208. package/dist/store/scopes/composer.d.ts.map +1 -1
  209. package/dist/store/scopes/message.d.ts +1 -1
  210. package/dist/store/scopes/queue-item.d.ts +2 -9
  211. package/dist/store/scopes/queue-item.d.ts.map +1 -1
  212. package/dist/store/scopes/queue-item.js +0 -6
  213. package/dist/types/message.d.ts +8 -4
  214. package/dist/types/message.d.ts.map +1 -1
  215. package/dist/types/message.js.map +1 -1
  216. package/dist/utils/data-url.d.ts +14 -1
  217. package/dist/utils/data-url.d.ts.map +1 -1
  218. package/dist/utils/data-url.js +13 -1
  219. package/dist/utils/data-url.js.map +1 -1
  220. package/dist/utils/invoke-user-callback.d.ts +5 -0
  221. package/dist/utils/invoke-user-callback.d.ts.map +1 -0
  222. package/dist/utils/invoke-user-callback.js +19 -0
  223. package/dist/utils/invoke-user-callback.js.map +1 -0
  224. package/dist/utils/normalizePartStatus.js.map +1 -1
  225. package/dist/utils/silent-runtime-action.d.ts +6 -0
  226. package/dist/utils/silent-runtime-action.d.ts.map +1 -0
  227. package/dist/utils/silent-runtime-action.js +7 -0
  228. package/dist/utils/silent-runtime-action.js.map +1 -0
  229. package/package.json +10 -10
  230. package/src/adapters/voice.test.ts +66 -1
  231. package/src/adapters/voice.ts +8 -0
  232. package/src/internal.ts +13 -0
  233. package/src/model-context/frame/host.test.ts +152 -9
  234. package/src/model-context/frame/host.ts +23 -5
  235. package/src/model-context/frame/provider.test.ts +601 -19
  236. package/src/model-context/frame/provider.ts +158 -20
  237. package/src/model-context/frame/types.ts +5 -0
  238. package/src/model-context/interactable-composer-metadata.ts +1 -1
  239. package/src/model-context/registry.test.ts +44 -0
  240. package/src/model-context/registry.ts +13 -35
  241. package/src/react/client/InMemoryThreadList.test.tsx +49 -0
  242. package/src/react/client/InMemoryThreadList.ts +7 -1
  243. package/src/react/client/Interactables.ts +19 -178
  244. package/src/react/client/RemoteThreadList.test.ts +292 -0
  245. package/src/react/client/RemoteThreadList.ts +119 -82
  246. package/src/react/interactables-legacy/Interactables.ts +20 -177
  247. package/src/react/interactables-shared/useInteractablePersistenceQueue.test.tsx +161 -0
  248. package/src/react/interactables-shared/useInteractablePersistenceQueue.ts +206 -0
  249. package/src/react/primitive-hooks/useActionBarCopy.test.ts +15 -0
  250. package/src/react/primitive-hooks/useActionBarCopy.ts +13 -3
  251. package/src/react/primitive-hooks/useThreadIsEmpty.ts +3 -0
  252. package/src/react/primitive-hooks/useThreadIsRunning.ts +3 -0
  253. package/src/react/runtimes/RemoteThreadListHookInstanceManager.tsx +12 -0
  254. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.republish.test.ts +87 -0
  255. package/src/react/runtimes/RemoteThreadListThreadListRuntimeCore.tsx +251 -76
  256. package/src/react/runtimes/RemoteThreadResource.ts +2 -0
  257. package/src/react/runtimes/assistant-transport/runManager.ts +4 -17
  258. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.test.tsx +58 -0
  259. package/src/react/runtimes/assistant-transport/useAssistantTransportRuntime.ts +3 -1
  260. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.test.tsx +92 -0
  261. package/src/react/runtimes/cloud/AssistantCloudThreadHistoryAdapter.ts +40 -141
  262. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.test.ts +37 -0
  263. package/src/react/runtimes/cloud/createCloudThreadListAdapterCreateFallback.ts +18 -0
  264. package/src/react/runtimes/cloud/useCloudThreadListAdapter.test.tsx +98 -3
  265. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +68 -6
  266. package/src/react/runtimes/external-message-converter.test.ts +0 -14
  267. package/src/react/runtimes/external-message-converter.test.tsx +77 -0
  268. package/src/react/runtimes/external-message-converter.ts +58 -481
  269. package/src/react/runtimes/useLocalRuntime.test.tsx +5 -5
  270. package/src/runtime/api/composer-runtime.ts +1 -1
  271. package/src/runtime/api/message-runtime.test.ts +28 -1
  272. package/src/runtime/base/base-composer-runtime-core.ts +20 -40
  273. package/src/runtime/base/base-thread-runtime-core.test.ts +16 -0
  274. package/src/runtime/base/base-thread-runtime-core.ts +6 -11
  275. package/src/runtime/base/default-thread-composer-runtime-core.ts +1 -4
  276. package/src/runtime/interfaces/composer-runtime-core.ts +1 -1
  277. package/src/runtime/interfaces/thread-runtime-core.ts +1 -1
  278. package/src/runtime/internal.ts +5 -0
  279. package/src/runtime/queue/external-thread-queue-adapter.ts +1 -1
  280. package/src/runtime/queue/message-queue.ts +1 -4
  281. package/src/runtime/queue/queue-item.ts +10 -0
  282. package/src/runtime/utils/abortable-thread-load.test.ts +87 -0
  283. package/src/runtime/utils/abortable-thread-load.ts +53 -0
  284. package/src/runtime/utils/attachment-add-operations.ts +59 -0
  285. package/src/runtime/utils/auto-status.ts +33 -0
  286. package/src/runtime/utils/external-message-conversion.test.ts +113 -0
  287. package/src/runtime/utils/external-message-conversion.ts +494 -0
  288. package/src/runtime/utils/external-store-message.ts +2 -0
  289. package/src/runtime/utils/message-repository-session.test.ts +195 -0
  290. package/src/runtime/utils/message-repository-session.ts +176 -0
  291. package/src/runtime/utils/message-repository.ts +33 -22
  292. package/src/runtime/utils/pending-tool-calls.test.ts +57 -0
  293. package/src/runtime/utils/pending-tool-calls.ts +35 -0
  294. package/src/runtime/utils/thread-message-like.ts +2 -0
  295. package/src/runtimes/external-store/external-store-thread-list-runtime-core.ts +6 -14
  296. package/src/runtimes/external-store/external-store-thread-runtime-core.ts +179 -28
  297. package/src/runtimes/inert/InertThreadRuntimeCore.test.ts +253 -0
  298. package/src/runtimes/inert/InertThreadRuntimeCore.ts +232 -0
  299. package/src/runtimes/local/local-thread-runtime-core.test.ts +491 -1
  300. package/src/runtimes/local/local-thread-runtime-core.ts +86 -11
  301. package/src/runtimes/readonly/ReadonlyThreadRuntimeCore.ts +17 -209
  302. package/src/runtimes/remote-thread-list/adapter-changed.ts +10 -0
  303. package/src/runtimes/remote-thread-list/empty-thread-core.ts +28 -221
  304. package/src/runtimes/remote-thread-list/optimistic-state.test.ts +18 -0
  305. package/src/runtimes/remote-thread-list/optimistic-state.ts +13 -0
  306. package/src/runtimes/remote-thread-list/remote-thread-state.test.ts +37 -0
  307. package/src/runtimes/remote-thread-list/remote-thread-state.ts +123 -0
  308. package/src/runtimes/remote-thread-list/types.ts +4 -0
  309. package/src/runtimes/tool-invocations/EDGE_CASES.md +17 -0
  310. package/src/runtimes/tool-invocations/ToolInvocationTracker.test.ts +140 -0
  311. package/src/runtimes/tool-invocations/ToolInvocationTracker.ts +3 -0
  312. package/src/store/clients/external-thread.ts +17 -62
  313. package/src/store/clients/single-thread-list.ts +4 -1
  314. package/src/store/clients/suggestions.test.ts +31 -0
  315. package/src/store/clients/suggestions.ts +18 -19
  316. package/src/store/runtime-clients/handle-runtime-action.test.ts +40 -0
  317. package/src/store/runtime-clients/handle-runtime-action.ts +3 -0
  318. package/src/store/scopes/message.ts +1 -1
  319. package/src/store/scopes/queue-item.ts +2 -9
  320. package/src/tests/MessageRepository.test.ts +125 -0
  321. package/src/tests/RemoteThreadListThreadListRuntimeCore-errors.test.ts +2 -2
  322. package/src/tests/RemoteThreadListThreadListRuntimeCore-load-race.test.ts +194 -0
  323. package/src/tests/RemoteThreadListThreadListRuntimeCore-loadMore.test.ts +8 -6
  324. package/src/tests/RemoteThreadListThreadListRuntimeCore-title-race.test.ts +158 -0
  325. package/src/tests/empty-thread-core.test.ts +6 -0
  326. package/src/tests/external-store-thread-runtime-core-adapter.test.ts +184 -0
  327. package/src/tests/external-store-thread-runtime-core.test.ts +642 -0
  328. package/src/tests/external-thread-attachments.test.tsx +173 -0
  329. package/src/tests/external-thread-isEmpty.test.tsx +100 -0
  330. package/src/tests/remote-thread-list-adapter-switch.test.ts +383 -0
  331. package/src/tests/single-thread-list-item.test.tsx +47 -0
  332. package/src/tests/thread-message-like.test.ts +39 -1
  333. package/src/tests/useRemoteThreadListRuntime-controlled.test.tsx +67 -0
  334. package/src/types/message.ts +9 -4
  335. package/src/utils/data-url.test.ts +52 -1
  336. package/src/utils/data-url.ts +21 -0
  337. package/src/utils/invoke-user-callback.test.ts +82 -0
  338. package/src/utils/invoke-user-callback.ts +31 -0
  339. package/src/utils/normalizePartStatus.ts +1 -1
  340. package/src/utils/silent-runtime-action.ts +6 -0
  341. package/dist/store/scopes/queue-item.js.map +0 -1
@@ -13,21 +13,25 @@ import {
13
13
  useClientResource,
14
14
  } from "@assistant-ui/store/client";
15
15
  import { isDevelopment, useThreadSelectionEvents } from "../../store/internal";
16
- import { generateId } from "../../utils/id";
17
16
  import { OptimisticState } from "../../runtimes/remote-thread-list/optimistic-state";
18
17
  import {
19
18
  classifyThreads,
19
+ createEmptyRemoteThreadState,
20
20
  createThreadMappingId,
21
21
  getThreadData,
22
22
  normalizeCursor,
23
23
  updateStatusReducer,
24
24
  type RemoteThreadData,
25
25
  type RemoteThreadState,
26
+ preserveMidLoadTransitions,
27
+ seedNewThread,
28
+ statusSnapshot,
26
29
  } from "../../runtimes/remote-thread-list/remote-thread-state";
27
30
  import type {
28
31
  RemoteThreadInitializeResponse,
29
32
  RemoteThreadListAdapter,
30
33
  } from "../../runtimes/remote-thread-list/types";
34
+ import { ThreadListAdapterChangedError } from "../../runtimes/remote-thread-list/adapter-changed";
31
35
  import type { ThreadMessage } from "../../types/message";
32
36
  import { AssistantMessageStream } from "assistant-stream";
33
37
  import { handleThreadListAction } from "../../store/runtime-clients/handle-thread-list-action";
@@ -39,20 +43,11 @@ import { AdaptedRemoteThread } from "./AdaptedRemoteThread";
39
43
 
40
44
  const RESOLVED_PROMISE = Promise.resolve();
41
45
 
42
- const EMPTY_LIST: RemoteThreadState = {
43
- isLoading: true,
44
- isLoadingMore: false,
45
- cursor: undefined,
46
- newThreadId: undefined,
47
- threadIds: [],
48
- archivedThreadIds: [],
49
- threadIdMap: {},
50
- threadData: {},
51
- };
46
+ const EMPTY_LIST = createEmptyRemoteThreadState();
52
47
 
53
48
  export type RemoteThreadListProps = {
54
49
  /**
55
- * Swapping this to a different backing store does not reload the list. Call `reload()` after a genuine swap. A recreated object for the same store is a no-op.
50
+ * Swapping this to a different backing store does not reload the list. Call `reload()` after a genuine swap. A recreated object for the same store is a no-op. `reload()` after a different adapter instance resets selection and cached records before loading.
56
51
  */
57
52
  adapter: RemoteThreadListAdapter;
58
53
  /**
@@ -90,46 +85,14 @@ const toInitializeResult = (
90
85
 
91
86
  const applyTitleStream = async (
92
87
  stream: Parameters<typeof AssistantMessageStream.fromAssistantStream>[0],
93
- onTitle: (title: string | undefined) => void,
88
+ onTitle: (title: string | undefined) => Promise<void>,
94
89
  ) => {
95
90
  const messageStream = AssistantMessageStream.fromAssistantStream(stream);
96
91
  for await (const result of messageStream) {
97
- onTitle(result.parts.filter((part) => part.type === "text")[0]?.text);
92
+ await onTitle(result.parts.filter((part) => part.type === "text")[0]?.text);
98
93
  }
99
94
  };
100
95
 
101
- const seedNewThread = (
102
- state: RemoteThreadState,
103
- ): { id: string; state: RemoteThreadState } => {
104
- let id: string;
105
- do {
106
- id = `__LOCALID_${generateId()}`;
107
- } while (state.threadIdMap[id]);
108
- const mappingId = createThreadMappingId(id);
109
- return {
110
- id,
111
- state: {
112
- ...state,
113
- newThreadId: id,
114
- threadIdMap: {
115
- ...state.threadIdMap,
116
- [id]: mappingId,
117
- },
118
- threadData: {
119
- ...state.threadData,
120
- [mappingId]: {
121
- status: "new",
122
- id,
123
- remoteId: undefined,
124
- externalId: undefined,
125
- title: undefined,
126
- custom: undefined,
127
- },
128
- },
129
- },
130
- };
131
- };
132
-
133
96
  const useThreadListItemClient = (props: {
134
97
  data: RemoteThreadData;
135
98
  isRunning: boolean;
@@ -359,6 +322,8 @@ const useRemoteThreadList = (
359
322
  initialMainId: seeded.id,
360
323
  session: {
361
324
  adapter,
325
+ adapterAtLoad: adapter,
326
+ adapterGeneration: 0,
362
327
  loadGeneration: 0,
363
328
  switchGeneration: 0,
364
329
  loadPromise: undefined as Promise<void> | undefined,
@@ -375,6 +340,9 @@ const useRemoteThreadList = (
375
340
  };
376
341
  });
377
342
 
343
+ // Config updates can invoke `reload()` in the same turn as the re-render, before the effect below runs.
344
+ session.adapter = adapter;
345
+
378
346
  const listState = useSyncExternalStore(
379
347
  (onStoreChange) => store.subscribe(onStoreChange),
380
348
  () => store.value,
@@ -417,19 +385,22 @@ const useRemoteThreadList = (
417
385
  const getLoadThreadsPromise = useCallback(() => {
418
386
  if (session.loadPromise) return session.loadPromise;
419
387
  const generation = session.loadGeneration;
388
+ const adapter = session.adapter;
389
+ const statusAtRequest = statusSnapshot(store.baseValue);
420
390
  session.loadPromise = store
421
391
  .optimisticUpdate({
422
- execute: () => session.adapter.list(),
392
+ execute: () => adapter.list(),
423
393
  loading: (state) => ({ ...state, isLoading: true }),
424
394
  then: (state, page) => {
425
395
  if (generation !== session.loadGeneration) return state;
396
+ session.adapterAtLoad = adapter;
426
397
  const fresh = classifyThreads(page.threads, {
427
398
  threadIds: [],
428
399
  archivedThreadIds: [],
429
400
  threadIdMap: {},
430
401
  threadData: {},
431
402
  });
432
- return {
403
+ const merged = {
433
404
  ...state,
434
405
  isLoading: false,
435
406
  cursor: normalizeCursor(page.nextCursor),
@@ -444,6 +415,7 @@ const useRemoteThreadList = (
444
415
  ...fresh.threadData,
445
416
  },
446
417
  };
418
+ return preserveMidLoadTransitions(merged, state, statusAtRequest);
447
419
  },
448
420
  })
449
421
  .catch((error: unknown) => {
@@ -460,15 +432,34 @@ const useRemoteThreadList = (
460
432
  }, [session, store]);
461
433
 
462
434
  const reload = useCallback(() => {
435
+ const adapterChanged = adapter !== session.adapterAtLoad;
463
436
  session.loadGeneration++;
464
437
  session.loadPromise = undefined;
465
438
  session.loadMorePromise = undefined;
466
- store.update({
467
- ...store.baseValue,
468
- cursor: undefined,
469
- });
439
+ if (adapterChanged) {
440
+ session.adapterGeneration++;
441
+ session.switchGeneration++;
442
+ session.switchTask = undefined;
443
+ session.adapterAtLoad = adapter;
444
+ const seeded = seedNewThread(EMPTY_LIST);
445
+ store.reset({ ...seeded.state, isLoading: true });
446
+ assignMainThreadId(seeded.id);
447
+ notifyRemoteId(undefined, true);
448
+ } else {
449
+ store.update({
450
+ ...store.baseValue,
451
+ cursor: undefined,
452
+ });
453
+ }
470
454
  return getLoadThreadsPromise();
471
- }, [getLoadThreadsPromise, session, store]);
455
+ }, [
456
+ adapter,
457
+ assignMainThreadId,
458
+ getLoadThreadsPromise,
459
+ notifyRemoteId,
460
+ session,
461
+ store,
462
+ ]);
472
463
 
473
464
  useEffect(() => {
474
465
  void getLoadThreadsPromise();
@@ -516,6 +507,7 @@ const useRemoteThreadList = (
516
507
  },
517
508
  })
518
509
  .catch((error: unknown) => {
510
+ if (generation !== session.loadGeneration) return;
519
511
  console.error("[assistant-ui] thread list loadMore failed:", error);
520
512
  })
521
513
  .then(() => {
@@ -689,18 +681,34 @@ const useRemoteThreadList = (
689
681
  [session, store, switchToNewThread],
690
682
  );
691
683
 
684
+ const requireAdapterGeneration = useCallback(
685
+ (generation: number) => {
686
+ if (generation !== session.adapterGeneration) {
687
+ throw new ThreadListAdapterChangedError();
688
+ }
689
+ },
690
+ [session],
691
+ );
692
+
692
693
  const initialize = useCallback(
693
694
  async (threadId: string) => {
695
+ const currentAdapter = session.adapter;
696
+ const adapterGeneration = session.adapterGeneration;
694
697
  if (store.value.newThreadId !== threadId) {
695
698
  const data = getThreadData(store.value, threadId);
696
699
  if (!data) throw threadNotFoundError(threadId, "initializing it");
697
700
  if (data.status === "new") {
698
701
  throw threadStatusError(threadId, data.status, "be initialized here");
699
702
  }
700
- return toInitializeResult(await data.initializeTask);
703
+ const result = toInitializeResult(await data.initializeTask);
704
+ requireAdapterGeneration(adapterGeneration);
705
+ return result;
701
706
  }
702
707
  const result = await store.optimisticUpdate({
703
- execute: () => session.adapter.initialize(threadId),
708
+ execute: () => {
709
+ requireAdapterGeneration(adapterGeneration);
710
+ return currentAdapter.initialize(threadId);
711
+ },
704
712
  optimistic: (state) => updateStatusReducer(state, threadId, "regular"),
705
713
  loading: (state, task) => {
706
714
  const mappingId = createThreadMappingId(threadId);
@@ -716,6 +724,7 @@ const useRemoteThreadList = (
716
724
  };
717
725
  },
718
726
  then: (state, { remoteId, externalId }) => {
727
+ if (adapterGeneration !== session.adapterGeneration) return state;
719
728
  const data = getThreadData(state, threadId);
720
729
  if (!data) return state;
721
730
  const mappingId = createThreadMappingId(threadId);
@@ -737,16 +746,19 @@ const useRemoteThreadList = (
737
746
  };
738
747
  },
739
748
  });
749
+ requireAdapterGeneration(adapterGeneration);
740
750
  if (threadId === session.mainThreadId) {
741
751
  notifyRemoteId(result.remoteId, true);
742
752
  }
743
753
  return toInitializeResult(result);
744
754
  },
745
- [notifyRemoteId, session, store],
755
+ [notifyRemoteId, requireAdapterGeneration, session, store],
746
756
  );
747
757
 
748
758
  const rename = useCallback(
749
759
  (threadIdOrRemoteId: string, newTitle: string) => {
760
+ const currentAdapter = session.adapter;
761
+ const adapterGeneration = session.adapterGeneration;
750
762
  const data = getThreadData(store.value, threadIdOrRemoteId);
751
763
  if (!data) throw threadNotFoundError(threadIdOrRemoteId, "renaming it");
752
764
  if (data.status === "new") {
@@ -755,7 +767,8 @@ const useRemoteThreadList = (
755
767
  return store.optimisticUpdate({
756
768
  execute: async () => {
757
769
  const { remoteId } = await data.initializeTask;
758
- return session.adapter.rename(remoteId, newTitle);
770
+ requireAdapterGeneration(adapterGeneration);
771
+ return currentAdapter.rename(remoteId, newTitle);
759
772
  },
760
773
  optimistic: (state) => {
761
774
  const current = getThreadData(state, threadIdOrRemoteId);
@@ -773,7 +786,7 @@ const useRemoteThreadList = (
773
786
  },
774
787
  });
775
788
  },
776
- [session, store],
789
+ [requireAdapterGeneration, session, store],
777
790
  );
778
791
 
779
792
  const updateCustom = useCallback(
@@ -781,6 +794,8 @@ const useRemoteThreadList = (
781
794
  threadIdOrRemoteId: string,
782
795
  custom: Record<string, unknown> | undefined,
783
796
  ) => {
797
+ const currentAdapter = session.adapter;
798
+ const adapterGeneration = session.adapterGeneration;
784
799
  const data = getThreadData(store.value, threadIdOrRemoteId);
785
800
  if (!data) {
786
801
  throw threadNotFoundError(
@@ -795,7 +810,7 @@ const useRemoteThreadList = (
795
810
  "update custom metadata",
796
811
  );
797
812
  }
798
- if (!session.adapter.updateCustom) {
813
+ if (!currentAdapter.updateCustom) {
799
814
  throw new Error(
800
815
  "Remote thread list adapter does not support updating custom metadata",
801
816
  );
@@ -803,7 +818,7 @@ const useRemoteThreadList = (
803
818
  return store.optimisticUpdate({
804
819
  execute: async () => {
805
820
  const { remoteId } = await data.initializeTask;
806
- const currentAdapter = session.adapter;
821
+ requireAdapterGeneration(adapterGeneration);
807
822
  if (!currentAdapter.updateCustom) {
808
823
  throw new Error(
809
824
  "Remote thread list adapter does not support updating custom metadata",
@@ -827,30 +842,36 @@ const useRemoteThreadList = (
827
842
  },
828
843
  });
829
844
  },
830
- [session, store],
845
+ [requireAdapterGeneration, session, store],
831
846
  );
832
847
 
833
848
  const archive = useCallback(
834
849
  async (threadIdOrRemoteId: string) => {
850
+ const currentAdapter = session.adapter;
851
+ const adapterGeneration = session.adapterGeneration;
835
852
  const data = getThreadData(store.value, threadIdOrRemoteId);
836
853
  if (!data) throw threadNotFoundError(threadIdOrRemoteId, "archiving it");
837
854
  if (data.status !== "regular") {
838
855
  throw threadStatusError(threadIdOrRemoteId, data.status, "be archived");
839
856
  }
840
857
  await ensureNotMain(data.id);
858
+ requireAdapterGeneration(adapterGeneration);
841
859
  return store.optimisticUpdate({
842
860
  execute: async () => {
843
861
  const { remoteId } = await data.initializeTask;
844
- return session.adapter.archive(remoteId);
862
+ requireAdapterGeneration(adapterGeneration);
863
+ return currentAdapter.archive(remoteId);
845
864
  },
846
865
  optimistic: (state) => updateStatusReducer(state, data.id, "archived"),
847
866
  });
848
867
  },
849
- [ensureNotMain, session, store],
868
+ [ensureNotMain, requireAdapterGeneration, session, store],
850
869
  );
851
870
 
852
871
  const unarchive = useCallback(
853
872
  (threadIdOrRemoteId: string) => {
873
+ const currentAdapter = session.adapter;
874
+ const adapterGeneration = session.adapterGeneration;
854
875
  const data = getThreadData(store.value, threadIdOrRemoteId);
855
876
  if (!data)
856
877
  throw threadNotFoundError(threadIdOrRemoteId, "unarchiving it");
@@ -865,36 +886,43 @@ const useRemoteThreadList = (
865
886
  execute: async () => {
866
887
  try {
867
888
  const { remoteId } = await data.initializeTask;
868
- return await session.adapter.unarchive(remoteId);
889
+ requireAdapterGeneration(adapterGeneration);
890
+ return await currentAdapter.unarchive(remoteId);
869
891
  } catch (error) {
870
- await ensureNotMain(data.id);
892
+ if (adapterGeneration === session.adapterGeneration) {
893
+ await ensureNotMain(data.id);
894
+ }
871
895
  throw error;
872
896
  }
873
897
  },
874
898
  optimistic: (state) => updateStatusReducer(state, data.id, "regular"),
875
899
  });
876
900
  },
877
- [ensureNotMain, session, store],
901
+ [ensureNotMain, requireAdapterGeneration, session, store],
878
902
  );
879
903
 
880
904
  const deleteThread = useCallback(
881
905
  async (threadIdOrRemoteId: string) => {
906
+ const currentAdapter = session.adapter;
907
+ const adapterGeneration = session.adapterGeneration;
882
908
  const data = getThreadData(store.value, threadIdOrRemoteId);
883
909
  if (!data) throw threadNotFoundError(threadIdOrRemoteId, "deleting it");
884
910
  if (data.status !== "regular" && data.status !== "archived") {
885
911
  throw threadStatusError(threadIdOrRemoteId, data.status, "be deleted");
886
912
  }
887
913
  await ensureNotMain(data.id);
914
+ requireAdapterGeneration(adapterGeneration);
888
915
  onDelete?.(data.id);
889
916
  return store.optimisticUpdate({
890
917
  execute: async () => {
891
918
  const { remoteId } = await data.initializeTask;
892
- return session.adapter.delete(remoteId);
919
+ requireAdapterGeneration(adapterGeneration);
920
+ return currentAdapter.delete(remoteId);
893
921
  },
894
922
  optimistic: (state) => updateStatusReducer(state, data.id, "deleted"),
895
923
  });
896
924
  },
897
- [ensureNotMain, onDelete, session, store],
925
+ [ensureNotMain, onDelete, requireAdapterGeneration, session, store],
898
926
  );
899
927
 
900
928
  const generateTitle = useCallback(
@@ -902,6 +930,8 @@ const useRemoteThreadList = (
902
930
  threadIdOrRemoteId: string,
903
931
  messages: readonly ThreadMessage[] | undefined,
904
932
  ) => {
933
+ const currentAdapter = session.adapter;
934
+ const adapterGeneration = session.adapterGeneration;
905
935
  const data = getThreadData(store.value, threadIdOrRemoteId);
906
936
  if (!data) {
907
937
  throw threadNotFoundError(threadIdOrRemoteId, "generating its title");
@@ -914,26 +944,33 @@ const useRemoteThreadList = (
914
944
  );
915
945
  }
916
946
  const { remoteId } = await data.initializeTask;
947
+ requireAdapterGeneration(adapterGeneration);
917
948
  if (!isSameThread(store.value, data.id, session.mainThreadId)) return;
918
949
  if (!messages) return;
919
- const stream = await session.adapter.generateTitle(remoteId, messages);
920
- await applyTitleStream(stream, (newTitle) => {
921
- const state = store.baseValue;
922
- const current = getThreadData(state, data.id);
923
- if (!current) return;
924
- store.update({
925
- ...state,
926
- threadData: {
927
- ...state.threadData,
928
- [current.id]: {
929
- ...current,
930
- title: newTitle,
931
- },
950
+ const stream = await currentAdapter.generateTitle(remoteId, messages);
951
+ requireAdapterGeneration(adapterGeneration);
952
+ await applyTitleStream(stream, async (newTitle) => {
953
+ await store.optimisticUpdate({
954
+ execute: async () => {},
955
+ optimistic: (state) => {
956
+ if (adapterGeneration !== session.adapterGeneration) return state;
957
+ const current = getThreadData(state, data.id);
958
+ if (!current) return state;
959
+ return {
960
+ ...state,
961
+ threadData: {
962
+ ...state.threadData,
963
+ [current.id]: {
964
+ ...current,
965
+ title: newTitle,
966
+ },
967
+ },
968
+ };
932
969
  },
933
970
  });
934
971
  });
935
972
  },
936
- [session, store],
973
+ [requireAdapterGeneration, session, store],
937
974
  );
938
975
 
939
976
  const { mainThreadClient, itemOrder, threadListItems } =
@@ -17,8 +17,7 @@ import { toJSONSchema, toPartialJSONSchema } from "assistant-stream";
17
17
  import { ModelContext } from "../../store";
18
18
  import { buildInteractableModelContext } from "./interactable-model-context";
19
19
  import { notifySubscribers as notifyStateSubscribers } from "../../subscribable/subscribable";
20
-
21
- const PERSISTENCE_DEBOUNCE_MS = 500;
20
+ import { useInteractablePersistenceQueue } from "../interactables-shared/useInteractablePersistenceQueue";
22
21
 
23
22
  const useInteractables = (): ClientOutput<"interactables"> => {
24
23
  const [state, setState] = useState<InteractablesState>(() => ({
@@ -48,26 +47,6 @@ const useInteractables = (): ClientOutput<"interactables"> => {
48
47
  const adapterRef = useRef<InteractablePersistenceAdapter | undefined>(
49
48
  undefined,
50
49
  );
51
- const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(
52
- undefined,
53
- );
54
- const syncSeqRef = useRef(0);
55
- const latestSyncSeqByIdRef = useRef(new Map<string, number>());
56
- const inFlightPersistenceRef = useRef(0);
57
- const flushResolversRef = useRef<Array<() => void>>([]);
58
- const dirtyIdsRef = useRef(new Set<string>());
59
-
60
- type PersistenceBatch = {
61
- adapter: InteractablePersistenceAdapter;
62
- payload: InteractablePersistedState;
63
- dirtyIds: Set<string>;
64
- seq: number;
65
- };
66
-
67
- const outgoingQueueRef = useRef<PersistenceBatch[]>([]);
68
- const runPersistenceRef = useRef<(batch?: PersistenceBatch) => void>(
69
- () => {},
70
- );
71
50
 
72
51
  const exportState = useCallback((): InteractablePersistedState => {
73
52
  const result: InteractablePersistedState = {};
@@ -77,139 +56,28 @@ const useInteractables = (): ClientOutput<"interactables"> => {
77
56
  return result;
78
57
  }, []);
79
58
 
80
- const takeDirtyBatch = useCallback(
81
- (adapter: InteractablePersistenceAdapter): PersistenceBatch | undefined => {
82
- if (dirtyIdsRef.current.size === 0) return;
83
- const dirtyIds = new Set(dirtyIdsRef.current);
84
- dirtyIdsRef.current.clear();
85
- const seq = ++syncSeqRef.current;
86
- for (const id of dirtyIds) latestSyncSeqByIdRef.current.set(id, seq);
87
- return { adapter, payload: exportState(), dirtyIds, seq };
88
- },
89
- [exportState],
90
- );
91
-
92
- const enqueuePersistence = useCallback(
93
- (adapter: InteractablePersistenceAdapter) => {
94
- const batch = takeDirtyBatch(adapter);
95
- if (!batch) return;
96
- if (inFlightPersistenceRef.current === 0) {
97
- runPersistenceRef.current(batch);
98
- } else {
99
- outgoingQueueRef.current.push(batch);
100
- }
101
- },
102
- [takeDirtyBatch],
103
- );
104
-
105
- const runPersistence = useCallback(
106
- async (batch?: PersistenceBatch) => {
107
- const resolved =
108
- batch ??
109
- (adapterRef.current ? takeDirtyBatch(adapterRef.current) : undefined);
110
- if (!resolved) {
111
- if (inFlightPersistenceRef.current === 0) {
112
- for (const resolve of flushResolversRef.current) resolve();
113
- flushResolversRef.current = [];
114
- }
115
- return;
116
- }
117
-
118
- const { adapter, payload, dirtyIds, seq } = resolved;
119
- inFlightPersistenceRef.current += 1;
120
-
121
- setStateAndRef((prev) => ({
122
- ...prev,
123
- persistence: {
124
- ...prev.persistence,
125
- ...Object.fromEntries(
126
- [...dirtyIds].map((id) => [
127
- id,
128
- { isPending: true, error: undefined },
129
- ]),
130
- ),
131
- },
132
- }));
133
-
134
- try {
135
- await adapter.save(payload);
136
- setStateAndRef((prev) => {
137
- let changed = false;
138
- const persistence = { ...prev.persistence };
139
- for (const id of dirtyIds) {
140
- if (
141
- latestSyncSeqByIdRef.current.get(id) !== seq ||
142
- dirtyIdsRef.current.has(id)
143
- )
144
- continue;
145
- latestSyncSeqByIdRef.current.delete(id);
146
- delete persistence[id];
147
- changed = true;
148
- }
149
- return changed ? { ...prev, persistence } : prev;
150
- });
151
- } catch (e) {
152
- setStateAndRef((prev) => {
153
- let changed = false;
154
- const persistence = { ...prev.persistence };
155
- for (const id of dirtyIds) {
156
- if (
157
- latestSyncSeqByIdRef.current.get(id) !== seq ||
158
- dirtyIdsRef.current.has(id)
159
- )
160
- continue;
161
- latestSyncSeqByIdRef.current.delete(id);
162
- persistence[id] = { isPending: false, error: e };
163
- changed = true;
164
- }
165
- return changed ? { ...prev, persistence } : prev;
166
- });
167
- } finally {
168
- inFlightPersistenceRef.current -= 1;
169
- const next =
170
- outgoingQueueRef.current.shift() ??
171
- (adapterRef.current && dirtyIdsRef.current.size > 0
172
- ? takeDirtyBatch(adapterRef.current)
173
- : undefined);
174
- if (next) {
175
- if (debounceTimerRef.current !== undefined) {
176
- clearTimeout(debounceTimerRef.current);
177
- debounceTimerRef.current = undefined;
178
- }
179
- runPersistenceRef.current(next);
180
- } else if (inFlightPersistenceRef.current === 0) {
181
- for (const resolve of flushResolversRef.current) resolve();
182
- flushResolversRef.current = [];
183
- }
184
- }
59
+ const updatePersistenceStatus = useCallback(
60
+ (
61
+ updater: (
62
+ prev: InteractablesState["persistence"],
63
+ ) => InteractablesState["persistence"],
64
+ ) => {
65
+ setStateAndRef((prev) => {
66
+ const persistence = updater(prev.persistence);
67
+ return persistence === prev.persistence
68
+ ? prev
69
+ : { ...prev, persistence };
70
+ });
185
71
  },
186
- [setStateAndRef, takeDirtyBatch],
72
+ [setStateAndRef],
187
73
  );
188
- runPersistenceRef.current = (nextBatch) => {
189
- void runPersistence(nextBatch);
190
- };
191
74
 
192
- const schedulePersistence = useCallback(
193
- (id: string) => {
194
- if (!adapterRef.current) return;
195
- dirtyIdsRef.current.add(id);
196
- if (debounceTimerRef.current !== undefined) {
197
- clearTimeout(debounceTimerRef.current);
198
- }
199
- debounceTimerRef.current = setTimeout(() => {
200
- debounceTimerRef.current = undefined;
201
- if (inFlightPersistenceRef.current === 0 && adapterRef.current) {
202
- enqueuePersistence(adapterRef.current);
203
- } else {
204
- debounceTimerRef.current = setTimeout(() => {
205
- debounceTimerRef.current = undefined;
206
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
207
- }, PERSISTENCE_DEBOUNCE_MS);
208
- }
209
- }, PERSISTENCE_DEBOUNCE_MS);
210
- },
211
- [enqueuePersistence],
212
- );
75
+ const { flushIfPending, schedulePersistence, flush } =
76
+ useInteractablePersistenceQueue({
77
+ adapterRef,
78
+ snapshot: exportState,
79
+ updatePersistenceStatus,
80
+ });
213
81
 
214
82
  const importState = useCallback(
215
83
  (saved: InteractablePersistedState) => {
@@ -231,31 +99,6 @@ const useInteractables = (): ClientOutput<"interactables"> => {
231
99
  [setStateAndRef],
232
100
  );
233
101
 
234
- const flushIfPending = useCallback(() => {
235
- if (debounceTimerRef.current !== undefined) {
236
- clearTimeout(debounceTimerRef.current);
237
- debounceTimerRef.current = undefined;
238
- }
239
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
240
- }, [enqueuePersistence]);
241
-
242
- const flush = useCallback(async () => {
243
- if (debounceTimerRef.current !== undefined) {
244
- clearTimeout(debounceTimerRef.current);
245
- debounceTimerRef.current = undefined;
246
- }
247
- const hasWork =
248
- inFlightPersistenceRef.current > 0 ||
249
- dirtyIdsRef.current.size > 0 ||
250
- outgoingQueueRef.current.length > 0;
251
- if (!hasWork) return;
252
- const p = new Promise<void>((resolve) => {
253
- flushResolversRef.current.push(resolve);
254
- });
255
- if (adapterRef.current) enqueuePersistence(adapterRef.current);
256
- return p;
257
- }, [enqueuePersistence]);
258
-
259
102
  const setPersistenceAdapter = useCallback(
260
103
  (adapter: InteractablePersistenceAdapter | undefined) => {
261
104
  if (adapterRef.current !== adapter) flushIfPending();