akanjs 3.0.2 → 3.1.0-rc.1

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 (395) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/base/baseEnv.ts +0 -4
  3. package/base/primitiveRegistry.ts +2 -2
  4. package/client/cookie.ts +5 -1
  5. package/client/csrLocation.ts +122 -0
  6. package/client/csrTypes.ts +36 -4
  7. package/client/device.ts +2 -1
  8. package/client/frameConfig.ts +78 -7
  9. package/client/index.ts +3 -0
  10. package/client/makePageProto.tsx +3 -2
  11. package/client/native/external-view.ts +1 -0
  12. package/client/route/RootLayoutDefinition.ts +7 -1
  13. package/client/route/RouteHead.ts +108 -0
  14. package/client/route/index.ts +1 -0
  15. package/client/router.ts +36 -23
  16. package/client/rscNavigation.ts +8 -2
  17. package/client/viewportMeta.ts +60 -0
  18. package/client/webManifest.ts +18 -0
  19. package/common/Logger.ts +7 -2
  20. package/common/PipelineNames.ts +31 -0
  21. package/common/PipelinePattern.ts +70 -0
  22. package/common/PipelinePlanSchema.ts +490 -0
  23. package/common/backoff.ts +14 -0
  24. package/common/index.ts +39 -0
  25. package/common/pipelinePlan.ts +259 -0
  26. package/common/routeConvention.ts +1 -0
  27. package/common/types.ts +17 -0
  28. package/constant/constantRegistry.ts +26 -6
  29. package/dictionary/base.dictionary.ts +12 -0
  30. package/dictionary/trans.ts +9 -1
  31. package/document/into.ts +5 -0
  32. package/fetch/client/fetchClient.ts +135 -31
  33. package/fetch/client/httpClient.ts +45 -0
  34. package/fetch/client/wsClient.ts +125 -16
  35. package/fetch/fetchType/buildFetch.type.ts +1 -0
  36. package/index.ts +243 -10
  37. package/package.json +1 -5
  38. package/server/akanApp.ts +5 -2
  39. package/server/akanServer.ts +44 -8
  40. package/server/artifact/builderRpc.ts +8 -3
  41. package/server/artifact/ipcTypes.ts +5 -2
  42. package/server/di/diLifecycle.ts +39 -3
  43. package/server/di/utils.ts +28 -1
  44. package/server/ed25519Key.ts +48 -0
  45. package/server/index.ts +1 -0
  46. package/server/machine/index.ts +2 -0
  47. package/server/machine/machineAccount.ts +27 -0
  48. package/server/machine/machineToken.ts +151 -0
  49. package/server/mcp/McpApps.ts +256 -0
  50. package/server/mcp/McpDispatcher.ts +31 -7
  51. package/server/mcp/McpRouter.ts +209 -46
  52. package/server/ops/opsToken.ts +3 -10
  53. package/server/resolver/database.resolver.ts +3 -0
  54. package/server/resolver/signal.resolver.ts +40 -3
  55. package/server/routeElementComposer.tsx +78 -2
  56. package/server/routeState.ts +6 -0
  57. package/server/routeTreeBuilder.ts +26 -9
  58. package/server/routing/apiRouter.ts +6 -3
  59. package/server/routing/roomReplay.ts +88 -0
  60. package/server/rscClient.tsx +53 -8
  61. package/server/rscWorker.tsx +141 -27
  62. package/server/rscWorkerHost.ts +18 -5
  63. package/server/sitemap.ts +14 -0
  64. package/server/ssrFromRscRenderer.tsx +12 -6
  65. package/server/ssrTypes.ts +3 -1
  66. package/server/systemPageDocument.tsx +38 -30
  67. package/server/types.tsx +2 -0
  68. package/server/webRouter.ts +74 -5
  69. package/service/adapt.ts +1 -1
  70. package/service/injectInfo.ts +9 -1
  71. package/service/ipcTypes.ts +2 -1
  72. package/service/predefinedAdaptor/database.adaptor.ts +1 -146
  73. package/service/predefinedAdaptor/queue.adaptor.ts +44 -6
  74. package/service/predefinedAdaptor/queueBackoff.ts +12 -0
  75. package/service/predefinedAdaptor/solidQueue.adaptor.ts +17 -13
  76. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +44 -2
  77. package/service/predefinedAdaptor/sql/dialect/postgres.ts +3 -0
  78. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +5 -2
  79. package/service/predefinedAdaptor/sql/search/fts5.ts +1 -1
  80. package/service/predefinedAdaptor/sql/types.ts +8 -6
  81. package/service/predefinedAdaptor/sql/values.ts +0 -23
  82. package/service/serve.ts +5 -1
  83. package/signal/agent.signal.ts +1 -1
  84. package/signal/base.signal.ts +2 -2
  85. package/signal/endpointIdempotency.ts +53 -0
  86. package/signal/internalArg.ts +14 -0
  87. package/signal/mcp/McpDocument.ts +38 -1
  88. package/signal/mcp/mcpProtocol.ts +18 -0
  89. package/signal/signalContext.ts +58 -30
  90. package/signal/signalRegistry.ts +14 -4
  91. package/signal/types.ts +18 -3
  92. package/store/action.ts +17 -19
  93. package/store/agent/StoreSurfaceSource.ts +1 -1
  94. package/store/agentic/StToolBuilder.ts +3 -0
  95. package/test/conformance.ts +9 -33
  96. package/types/base/baseEnv.d.ts +0 -4
  97. package/types/base/primitiveRegistry.d.ts +1 -1
  98. package/types/client/csrLocation.d.ts +32 -0
  99. package/types/client/csrTypes.d.ts +43 -4
  100. package/types/client/frameConfig.d.ts +14 -2
  101. package/types/client/index.d.ts +3 -0
  102. package/types/client/native/external-view.d.ts +1 -0
  103. package/types/client/route/RootLayoutDefinition.d.ts +2 -1
  104. package/types/client/route/RouteHead.d.ts +13 -0
  105. package/types/client/route/index.d.ts +1 -0
  106. package/types/client/router.d.ts +6 -0
  107. package/types/client/rscNavigation.d.ts +4 -0
  108. package/types/client/viewportMeta.d.ts +18 -0
  109. package/types/client/webManifest.d.ts +4 -0
  110. package/types/common/PipelineNames.d.ts +6 -0
  111. package/types/common/PipelinePattern.d.ts +14 -0
  112. package/types/common/PipelinePlanSchema.d.ts +9 -0
  113. package/types/common/backoff.d.ts +10 -0
  114. package/types/common/index.d.ts +5 -0
  115. package/types/common/pipelinePlan.d.ts +224 -0
  116. package/types/common/types.d.ts +16 -0
  117. package/types/constant/constantRegistry.d.ts +5 -2
  118. package/types/dictionary/base.dictionary.d.ts +1 -1
  119. package/types/dictionary/dictionary.d.ts +8 -8
  120. package/types/document/into.d.ts +9 -0
  121. package/types/fetch/client/fetchClient.d.ts +31 -4
  122. package/types/fetch/client/httpClient.d.ts +8 -0
  123. package/types/fetch/client/wsClient.d.ts +26 -2
  124. package/types/fetch/fetchType/buildFetch.type.d.ts +1 -0
  125. package/types/index.d.ts +230 -9
  126. package/types/server/akanServer.d.ts +16 -1
  127. package/types/server/artifact/builderRpc.d.ts +7 -2
  128. package/types/server/artifact/ipcTypes.d.ts +5 -1
  129. package/types/server/di/utils.d.ts +1 -0
  130. package/types/server/ed25519Key.d.ts +13 -0
  131. package/types/server/index.d.ts +1 -0
  132. package/types/server/machine/index.d.ts +2 -0
  133. package/types/server/machine/machineAccount.d.ts +5 -0
  134. package/types/server/machine/machineToken.d.ts +29 -0
  135. package/types/server/machine.d.ts +1 -0
  136. package/types/server/mcp/McpApps.d.ts +50 -0
  137. package/types/server/mcp/McpDispatcher.d.ts +4 -1
  138. package/types/server/mcp/McpRouter.d.ts +4 -0
  139. package/types/server/resolver/signal.resolver.d.ts +2 -1
  140. package/types/server/routeElementComposer.d.ts +24 -4
  141. package/types/server/routeState.d.ts +4 -1
  142. package/types/server/routing/roomReplay.d.ts +24 -0
  143. package/types/server/rscWorkerHost.d.ts +5 -2
  144. package/types/server/sitemap.d.ts +2 -0
  145. package/types/server/ssrFromRscRenderer.d.ts +3 -3
  146. package/types/server/ssrTypes.d.ts +4 -1
  147. package/types/server/systemPageDocument.d.ts +2 -0
  148. package/types/server/types.d.ts +4 -1
  149. package/types/server/webRouter.d.ts +3 -1
  150. package/types/service/adapt.d.ts +1 -1
  151. package/types/service/ipcTypes.d.ts +2 -0
  152. package/types/service/predefinedAdaptor/database.adaptor.d.ts +1 -24
  153. package/types/service/predefinedAdaptor/queue.adaptor.d.ts +10 -3
  154. package/types/service/predefinedAdaptor/queueBackoff.d.ts +4 -0
  155. package/types/service/predefinedAdaptor/solidQueue.adaptor.d.ts +3 -3
  156. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +6 -1
  157. package/types/service/predefinedAdaptor/sql/dialect/postgres.d.ts +1 -0
  158. package/types/service/predefinedAdaptor/sql/dialect/sqlite.d.ts +1 -0
  159. package/types/service/predefinedAdaptor/sql/search/fts5.d.ts +1 -1
  160. package/types/service/predefinedAdaptor/sql/types.d.ts +9 -5
  161. package/types/service/predefinedAdaptor/sql/values.d.ts +0 -3
  162. package/types/service/serve.d.ts +3 -0
  163. package/types/signal/endpointIdempotency.d.ts +7 -0
  164. package/types/signal/internalArg.d.ts +10 -0
  165. package/types/signal/mcp/McpDocument.d.ts +8 -0
  166. package/types/signal/mcp/mcpProtocol.d.ts +17 -0
  167. package/types/signal/signalContext.d.ts +5 -3
  168. package/types/signal/signalRegistry.d.ts +4 -4
  169. package/types/signal/types.d.ts +21 -1
  170. package/types/store/agentic/StToolBuilder.d.ts +2 -0
  171. package/types/test/conformance.d.ts +7 -10
  172. package/types/ui/Agent/Chat.d.ts +5 -1
  173. package/types/ui/Agent/EmbedApproval.d.ts +1 -0
  174. package/types/ui/Agent/index.d.ts +3 -0
  175. package/types/ui/Agent/useKeyboardFrame.d.ts +2 -0
  176. package/types/ui/Agent/useScreenActions.d.ts +18 -0
  177. package/types/ui/BottomSheet.d.ts +1 -0
  178. package/types/ui/Constant/Doc.d.ts +11 -17
  179. package/types/ui/Constant/index.d.ts +7 -2
  180. package/types/ui/Constant/schemaDoc.d.ts +11 -0
  181. package/types/ui/Dialog/Modal.d.ts +6 -1
  182. package/types/ui/Dialog/index.d.ts +1 -1
  183. package/types/ui/Dialog/modalSurface.d.ts +12 -0
  184. package/types/ui/Layout/BottomInset.d.ts +1 -1
  185. package/types/ui/Layout/BottomTab.d.ts +11 -2
  186. package/types/ui/Layout/FrameSlotFallback.d.ts +9 -0
  187. package/types/ui/Layout/Navbar.d.ts +4 -1
  188. package/types/ui/Layout/TopInset.d.ts +1 -1
  189. package/types/ui/Layout/index.d.ts +4 -4
  190. package/types/ui/Link/Action.d.ts +5 -1
  191. package/types/ui/Link/Anchor.d.ts +5 -3
  192. package/types/ui/Link/activePath.d.ts +13 -0
  193. package/types/ui/Link/index.d.ts +1 -1
  194. package/types/ui/Modal.d.ts +6 -1
  195. package/types/ui/Model/EditModal.d.ts +6 -1
  196. package/types/ui/Model/New.d.ts +6 -1
  197. package/types/ui/Model/ViewEditModal.d.ts +6 -1
  198. package/types/ui/Model/ViewModal.d.ts +6 -1
  199. package/types/ui/Model/newFormOrigin.d.ts +7 -0
  200. package/types/ui/Portal.d.ts +3 -1
  201. package/types/ui/Reference/origin.d.ts +12 -0
  202. package/types/ui/Signal/Doc.d.ts +23 -18
  203. package/types/ui/Signal/Listener.d.ts +2 -6
  204. package/types/ui/Signal/Object.d.ts +6 -10
  205. package/types/ui/Signal/Response.d.ts +3 -6
  206. package/types/ui/Signal/RestApi.d.ts +11 -15
  207. package/types/ui/Signal/WebSocket.d.ts +11 -21
  208. package/types/ui/Signal/endpointEntries.d.ts +8 -0
  209. package/types/ui/Signal/index.d.ts +35 -12
  210. package/types/ui/Signal/signalText.d.ts +178 -0
  211. package/types/ui/System/CSR.d.ts +2 -2
  212. package/types/ui/System/Client.d.ts +4 -3
  213. package/types/ui/System/Common.d.ts +27 -6
  214. package/types/ui/System/SSR.d.ts +6 -6
  215. package/types/ui/Tooltip.d.ts +4 -2
  216. package/types/ui/index.d.ts +2 -1
  217. package/types/ui/overlayLayer.d.ts +6 -0
  218. package/types/ui/overlayPosition.d.ts +17 -0
  219. package/types/vendor/@akanjs/native/packages/cli/src/api.d.ts +14 -3
  220. package/types/vendor/@akanjs/native/packages/cli/src/commands/update.d.ts +1 -1
  221. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +30 -5
  222. package/types/vendor/@akanjs/native/packages/cli/src/lib/compat.d.ts +8 -0
  223. package/types/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.d.ts +5 -0
  224. package/types/vendor/@akanjs/native/packages/cli/src/lib/distribution.d.ts +22 -0
  225. package/types/vendor/@akanjs/native/packages/cli/src/lib/project.d.ts +4 -0
  226. package/types/vendor/@akanjs/native/packages/cli/src/lib/publish.d.ts +16 -10
  227. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +51 -7
  228. package/types/vendor/@akanjs/native/packages/cli/src/lib/vectors.d.ts +1 -1
  229. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.d.ts +12 -4
  230. package/types/vendor/@akanjs/native/packages/cli/src/platforms/windows.d.ts +5 -0
  231. package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +36 -0
  232. package/types/vendor/@akanjs/native/packages/desktop/src/dispatcher.d.ts +6 -0
  233. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +2 -0
  234. package/types/vendor/@akanjs/native/packages/desktop/src/server.d.ts +11 -0
  235. package/types/vendor/@akanjs/native/plugins/external-view/src/desktop.d.ts +17 -0
  236. package/types/vendor/@akanjs/native/plugins/external-view/src/index.d.ts +132 -0
  237. package/types/vendor/@akanjs/native/plugins/external-view/src.d.ts +1 -0
  238. package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +1 -1
  239. package/types/vendor/@akanjs/native/plugins/updates/src/index.d.ts +8 -0
  240. package/types/vendor/@akanjs/native/plugins/window/src/desktop.d.ts +4 -1
  241. package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +49 -0
  242. package/types/vendor/use-agentic/AgentActivity.d.ts +3 -1
  243. package/types/vendor/use-agentic/AgenticSurface.d.ts +2 -2
  244. package/types/vendor/use-agentic/types.d.ts +2 -0
  245. package/types/webkit/CsrStack.d.ts +1 -1
  246. package/types/webkit/EmbeddedLayout.d.ts +10 -0
  247. package/types/webkit/McpAppAgent.d.ts +14 -0
  248. package/types/webkit/McpAppApprovals.d.ts +8 -0
  249. package/types/webkit/McpAppBridge.d.ts +101 -0
  250. package/types/webkit/McpAppView.d.ts +21 -0
  251. package/types/webkit/RenderLayer.d.ts +3 -1
  252. package/types/webkit/index.d.ts +1 -0
  253. package/types/webkit/useLocation.d.ts +1 -1
  254. package/types/webkit/useVisualViewportFrame.d.ts +13 -0
  255. package/ui/Agent/Chat.tsx +29 -10
  256. package/ui/Agent/Composer.tsx +2 -1
  257. package/ui/Agent/EmbedApproval.tsx +17 -0
  258. package/ui/Agent/RichInput.tsx +1 -1
  259. package/ui/Agent/index.ts +2 -0
  260. package/ui/Agent/useKeyboardFrame.ts +10 -0
  261. package/ui/Agent/useScreenActions.ts +45 -0
  262. package/ui/BottomSheet.tsx +82 -53
  263. package/ui/Constant/Doc.tsx +113 -60
  264. package/ui/Constant/index.ts +2 -2
  265. package/ui/Constant/schemaDoc.ts +86 -26
  266. package/ui/Dialog/LegacyModal.tsx +53 -52
  267. package/ui/Dialog/Modal.tsx +93 -49
  268. package/ui/Dialog/modalSurface.ts +61 -0
  269. package/ui/DragAction.tsx +1 -1
  270. package/ui/DraggableList.tsx +1 -1
  271. package/ui/Dropdown.tsx +4 -6
  272. package/ui/KeyboardAvoiding.tsx +1 -1
  273. package/ui/Layout/BottomInset.tsx +32 -16
  274. package/ui/Layout/BottomTab.tsx +42 -16
  275. package/ui/Layout/FrameSlotFallback.tsx +59 -0
  276. package/ui/Layout/Navbar.tsx +56 -15
  277. package/ui/Layout/Sider.tsx +1 -1
  278. package/ui/Layout/TopInset.tsx +22 -17
  279. package/ui/Link/Action.tsx +15 -5
  280. package/ui/Link/Anchor.tsx +25 -13
  281. package/ui/Link/activePath.ts +25 -0
  282. package/ui/Link/index.tsx +1 -1
  283. package/ui/Load/Pagination.tsx +1 -1
  284. package/ui/Load/Units.tsx +1 -1
  285. package/ui/Loading/ProgressBar.tsx +1 -1
  286. package/ui/Modal.tsx +9 -0
  287. package/ui/Model/EditModal.tsx +16 -1
  288. package/ui/Model/New.tsx +25 -2
  289. package/ui/Model/NewWrapper_Client.tsx +2 -0
  290. package/ui/Model/ViewEditModal.tsx +25 -10
  291. package/ui/Model/ViewModal.tsx +16 -1
  292. package/ui/Model/newFormOrigin.ts +45 -0
  293. package/ui/Popconfirm.tsx +5 -7
  294. package/ui/Portal.tsx +4 -7
  295. package/ui/Reference/origin.ts +31 -0
  296. package/ui/ScreenNavigator.tsx +1 -1
  297. package/ui/Select.tsx +14 -14
  298. package/ui/Signal/Arg.tsx +2 -2
  299. package/ui/Signal/Doc.tsx +125 -63
  300. package/ui/Signal/Endpoint.tsx +11 -10
  301. package/ui/Signal/Listener.tsx +7 -10
  302. package/ui/Signal/Object.tsx +7 -14
  303. package/ui/Signal/Response.tsx +24 -24
  304. package/ui/Signal/RestApi.tsx +63 -52
  305. package/ui/Signal/WebSocket.tsx +65 -72
  306. package/ui/Signal/endpointEntries.ts +25 -0
  307. package/ui/Signal/index.ts +23 -6
  308. package/ui/Signal/signalText.ts +59 -0
  309. package/ui/System/CSR.tsx +41 -9
  310. package/ui/System/Client.tsx +37 -5
  311. package/ui/System/Common.tsx +76 -32
  312. package/ui/System/SSR.tsx +73 -31
  313. package/ui/Toast.tsx +1 -1
  314. package/ui/Tooltip.tsx +84 -30
  315. package/ui/index.ts +11 -1
  316. package/ui/overlayLayer.ts +25 -2
  317. package/ui/overlayPosition.ts +90 -0
  318. package/ui/recipe/inputRecipe.ts +8 -1
  319. package/ui/styles.css +152 -0
  320. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +22 -2
  321. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +37 -0
  322. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdateReports.kt +169 -0
  323. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeUpdates.kt +110 -38
  324. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +20 -0
  325. package/vendor/@akanjs/native/native/desktop/Cargo.lock +16 -16
  326. package/vendor/@akanjs/native/native/desktop/src/external.rs +651 -0
  327. package/vendor/@akanjs/native/native/desktop/src/json.rs +23 -0
  328. package/vendor/@akanjs/native/native/desktop/src/lib.rs +228 -5
  329. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeApp.swift +2 -1
  330. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +47 -1
  331. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdateReports.swift +145 -0
  332. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeUpdates.swift +112 -43
  333. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +18 -0
  334. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +24 -2
  335. package/vendor/@akanjs/native/packages/cli/src/api.ts +22 -6
  336. package/vendor/@akanjs/native/packages/cli/src/commands/build.ts +24 -8
  337. package/vendor/@akanjs/native/packages/cli/src/commands/update.ts +11 -9
  338. package/vendor/@akanjs/native/packages/cli/src/config.ts +35 -7
  339. package/vendor/@akanjs/native/packages/cli/src/lib/compat.ts +2 -0
  340. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +6 -4
  341. package/vendor/@akanjs/native/packages/cli/src/lib/desktop-server.ts +11 -1
  342. package/vendor/@akanjs/native/packages/cli/src/lib/distribution.ts +73 -0
  343. package/vendor/@akanjs/native/packages/cli/src/lib/project.ts +15 -3
  344. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +70 -23
  345. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +159 -23
  346. package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +2 -0
  347. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +9 -4
  348. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop-update.ts +1 -1
  349. package/vendor/@akanjs/native/packages/cli/src/platforms/desktop.ts +4 -2
  350. package/vendor/@akanjs/native/packages/cli/src/platforms/ios.ts +9 -4
  351. package/vendor/@akanjs/native/packages/cli/src/platforms/windows-installer.ts +60 -21
  352. package/vendor/@akanjs/native/packages/cli/src/platforms/windows.ts +14 -3
  353. package/vendor/@akanjs/native/packages/core/src/kernel.ts +49 -0
  354. package/vendor/@akanjs/native/packages/core/vectors/updates.json +31 -0
  355. package/vendor/@akanjs/native/packages/desktop/src/dispatcher.ts +13 -5
  356. package/vendor/@akanjs/native/packages/desktop/src/main.ts +3 -0
  357. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +2 -0
  358. package/vendor/@akanjs/native/packages/desktop/src/server.ts +70 -6
  359. package/vendor/@akanjs/native/plugins/external-view/native-plugin.json +23 -0
  360. package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +321 -0
  361. package/vendor/@akanjs/native/plugins/external-view/src/index.ts +106 -0
  362. package/vendor/@akanjs/native/plugins/single-instance/src/desktop.ts +4 -0
  363. package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +12 -5
  364. package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +133 -41
  365. package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +101 -36
  366. package/vendor/@akanjs/native/plugins/updates/native-plugin.json +4 -2
  367. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +15 -1
  368. package/vendor/@akanjs/native/plugins/updates/src/index.ts +7 -1
  369. package/vendor/@akanjs/native/plugins/window/native-plugin.json +4 -0
  370. package/vendor/@akanjs/native/plugins/window/src/desktop.ts +68 -1
  371. package/vendor/@akanjs/native/plugins/window/src/index.ts +47 -0
  372. package/vendor/use-agentic/AgentActivity.tsx +4 -2
  373. package/vendor/use-agentic/AgenticSurface.ts +3 -3
  374. package/vendor/use-agentic/ToolRunner.ts +8 -1
  375. package/vendor/use-agentic/types.ts +2 -0
  376. package/webkit/CsrRouteTable.ts +9 -1
  377. package/webkit/CsrStack.ts +9 -2
  378. package/webkit/EmbeddedLayout.tsx +77 -0
  379. package/webkit/McpAppAgent.ts +138 -0
  380. package/webkit/McpAppApprovals.ts +56 -0
  381. package/webkit/McpAppBridge.ts +291 -0
  382. package/webkit/McpAppView.ts +258 -0
  383. package/webkit/RenderLayer.tsx +20 -5
  384. package/webkit/bootCsr.tsx +54 -5
  385. package/webkit/index.ts +1 -0
  386. package/webkit/useCsrValues.ts +16 -10
  387. package/webkit/useFrameRuntime.ts +10 -1
  388. package/webkit/useLocation.ts +4 -3
  389. package/webkit/useVisualViewportFrame.tsx +52 -0
  390. package/service/predefinedAdaptor/sql/driver/libsql.ts +0 -45
  391. package/types/service/predefinedAdaptor/sql/driver/libsql.d.ts +0 -20
  392. package/types/ui/Agent/useKeyboardInset.d.ts +0 -1
  393. package/types/ui/ServerPortal.d.ts +0 -15
  394. package/ui/Agent/useKeyboardInset.ts +0 -19
  395. package/ui/ServerPortal.tsx +0 -49
@@ -4,6 +4,7 @@ import {
4
4
  type FetchPolicy,
5
5
  fileUploadContract,
6
6
  Logger,
7
+ type RetryPolicy,
7
8
  readAuthToken,
8
9
  resolveFileUploadCapability,
9
10
  } from "akanjs/common";
@@ -28,10 +29,10 @@ import {
28
29
  import type { GetSliceMetaObjFromDatabaseSignals } from "../types";
29
30
  import { FetchHandle } from "./fetchHandle";
30
31
  import { HttpClient } from "./httpClient";
31
- import type { ErrorConstructor } from "./remoteError";
32
+ import type { ErrorConstructor, RestoredError } from "./remoteError";
32
33
  import { SliceInitHandle } from "./sliceInitHandle";
33
34
  import { expandQueryArgs, normalizeQueryArgs } from "./sliceQueryArgs";
34
- import { WsClient } from "./wsClient";
35
+ import { WsClient, type WsClientOptions } from "./wsClient";
35
36
 
36
37
  type FetchHandler = (...args: unknown[]) => PromiseOrObject<unknown>;
37
38
  type FetchHandlerFactory = () => FetchHandler;
@@ -67,6 +68,21 @@ const sharedSignalRegistry: SharedSignalRegistry = globalWithSharedSignal[SHARED
67
68
  };
68
69
  globalWithSharedSignal[SHARED_SIGNAL_KEY] = sharedSignalRegistry;
69
70
 
71
+ export interface FetchBridgeCall {
72
+ key: string;
73
+ type: "query" | "mutation";
74
+ args: UnknownRecord;
75
+ returns: SerializedReturns;
76
+ }
77
+
78
+ /** A transport that is not HTTP: a view inside an MCP host has no network, and reaches the server through the host. */
79
+ export interface FetchBridge {
80
+ call(request: FetchBridgeCall): Promise<unknown>;
81
+ }
82
+
83
+ const FETCH_BRIDGE_KEY = Symbol.for("akanjs.fetch.bridge");
84
+ const globalWithFetchBridge = globalThis as typeof globalThis & { [FETCH_BRIDGE_KEY]?: FetchBridge | null };
85
+
70
86
  type ClientSignalMap<SigType extends { fetch: any }> = {
71
87
  [K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
72
88
  ? K
@@ -89,13 +105,17 @@ export class FetchClient {
89
105
  readonly sortValueMap = new Map<string, { [key: string]: { [path: string]: 1 | -1 } }>();
90
106
  readonly filterQueryMap = new Map<string, { [queryKey: string]: SerializedArg[] }>();
91
107
  readonly #originWs = new Map<string, WsClient>();
108
+ #wsOptions: WsClientOptions = {};
92
109
  readonly #handlerStore: Record<string, FetchHandler> = {};
93
110
  readonly #handlerFactory = new Map<string, FetchHandlerFactory>();
94
111
  #sharedRegistryAppliedVersion = 0;
95
112
  #clientIp: string | null = null;
113
+ /** Cloned toward another server: the request being served is this server's visitor, not that server's caller. */
114
+ #foreign = false;
96
115
  static #testClientIps = false;
97
116
  static #testClientIpSeq = 0;
98
117
  serializedSignal: { [key: string]: SerializedSignal } = {};
118
+ #ownSignal: { [key: string]: SerializedSignal } = {};
99
119
  jwt: string | null = null;
100
120
 
101
121
  constructor(
@@ -119,6 +139,13 @@ export class FetchClient {
119
139
  sharedSignalRegistry.signal = {};
120
140
  sharedSignalRegistry.version++;
121
141
  }
142
+ /** Every client in this tab sends its queries and mutations through `bridge`, and opens no socket; `null` restores HTTP. */
143
+ static setBridge(bridge: FetchBridge | null) {
144
+ globalWithFetchBridge[FETCH_BRIDGE_KEY] = bridge;
145
+ }
146
+ static get bridged() {
147
+ return !!globalWithFetchBridge[FETCH_BRIDGE_KEY];
148
+ }
122
149
  static resetSharedClient() {
123
150
  sharedClientState.proxy = null;
124
151
  sharedClientState.origin = null;
@@ -151,27 +178,43 @@ export class FetchClient {
151
178
  signal: SerializedSignal,
152
179
  ) {
153
180
  const current = serializedSignal[refName];
154
- serializedSignal[refName] = current
155
- ? {
156
- ...current,
157
- ...signal,
158
- endpoint: { ...current.endpoint, ...signal.endpoint },
159
- slice: current.slice || signal.slice ? { ...current.slice, ...signal.slice } : undefined,
160
- filter:
161
- current.filter || signal.filter
162
- ? {
163
- filter: { ...current.filter?.filter, ...signal.filter?.filter },
164
- sortKeys: [...new Set([...(current.filter?.sortKeys ?? []), ...(signal.filter?.sortKeys ?? [])])],
165
- sorts: { ...current.filter?.sorts, ...signal.filter?.sorts },
166
- }
167
- : undefined,
168
- getGuards: signal.getGuards ?? current.getGuards,
169
- cruGuards: signal.cruGuards ?? current.cruGuards,
170
- createGuards: signal.createGuards ?? current.createGuards,
171
- updateGuards: signal.updateGuards ?? current.updateGuards,
172
- removeGuards: signal.removeGuards ?? current.removeGuards,
173
- }
174
- : signal;
181
+ serializedSignal[refName] = current ? FetchClient.#definedOnly(FetchClient.#merged(current, signal)) : signal;
182
+ }
183
+
184
+ static #definedOnly(signal: SerializedSignal): SerializedSignal {
185
+ return Object.fromEntries(Object.entries(signal).filter(([, value]) => value !== undefined)) as SerializedSignal;
186
+ }
187
+ static #merged(current: SerializedSignal, signal: SerializedSignal): SerializedSignal {
188
+ return {
189
+ ...current,
190
+ ...signal,
191
+ endpoint: { ...current.endpoint, ...signal.endpoint },
192
+ slice: current.slice || signal.slice ? { ...current.slice, ...signal.slice } : undefined,
193
+ filter:
194
+ current.filter || signal.filter
195
+ ? {
196
+ filter: { ...current.filter?.filter, ...signal.filter?.filter },
197
+ sortKeys: [...new Set([...(current.filter?.sortKeys ?? []), ...(signal.filter?.sortKeys ?? [])])],
198
+ sorts: { ...current.filter?.sorts, ...signal.filter?.sorts },
199
+ }
200
+ : undefined,
201
+ getGuards: signal.getGuards ?? current.getGuards,
202
+ cruGuards: signal.cruGuards ?? current.cruGuards,
203
+ createGuards: signal.createGuards ?? current.createGuards,
204
+ updateGuards: signal.updateGuards ?? current.updateGuards,
205
+ removeGuards: signal.removeGuards ?? current.removeGuards,
206
+ origin:
207
+ current.origin || signal.origin
208
+ ? [...new Set([...(current.origin ?? []), ...(signal.origin ?? [])])]
209
+ : undefined,
210
+ };
211
+ }
212
+ /**
213
+ * What this client was built with and applied itself, never what it synced from another client in the process:
214
+ * a `getSerializedSignal` macro reads this, so a lib's bundle cannot depend on which app's signals loaded first.
215
+ */
216
+ get ownSerializedSignal(): { [key: string]: SerializedSignal } {
217
+ return this.#ownSignal;
175
218
  }
176
219
  /** For calls neither the caller nor the endpoint budgets; `false` waits as long as the runtime will (minutes). */
177
220
  setTimeout(timeout?: number | false) {
@@ -185,8 +228,10 @@ export class FetchClient {
185
228
  }
186
229
  applySignal(serializedSignal: { [key: string]: SerializedSignal }, { share = true }: { share?: boolean } = {}) {
187
230
  if (share && Object.keys(serializedSignal).length > 0) {
188
- for (const [refName, signal] of Object.entries(serializedSignal))
231
+ for (const [refName, signal] of Object.entries(serializedSignal)) {
189
232
  FetchClient.#mergeSerializedSignalInto(sharedSignalRegistry.signal, refName, signal);
233
+ FetchClient.#mergeSerializedSignalInto(this.#ownSignal, refName, signal);
234
+ }
190
235
  sharedSignalRegistry.version++;
191
236
  this.#sharedRegistryAppliedVersion = sharedSignalRegistry.version;
192
237
  }
@@ -246,7 +291,9 @@ export class FetchClient {
246
291
  this.#handlerFactory.set(key, factory);
247
292
  delete this.#handlerStore[key];
248
293
  }
294
+
249
295
  connect() {
296
+ if (FetchClient.bridged) return;
250
297
  this.ws.connect();
251
298
  }
252
299
  disconnect() {
@@ -260,14 +307,23 @@ export class FetchClient {
260
307
  connect = !(origin && typeof window === "undefined"),
261
308
  jwt,
262
309
  clientIp,
310
+ ws,
311
+ retry,
263
312
  }: {
264
313
  origin?: string;
265
314
  connect?: boolean;
266
315
  jwt?: string;
267
316
  /** Sent as `x-real-ip`, which a server believes only from a trusted proxy — a test's loopback is one. */
268
317
  clientIp?: string | null;
318
+ /** The clone's sockets: reconnect backoff, upgrade headers and TLS, and state, revocation and error hooks. */
319
+ ws?: WsClientOptions;
320
+ /** Every call's `retry` unless it names its own: transient failures, a mutation under one Idempotency-Key. */
321
+ retry?: RetryPolicy | false;
269
322
  } = {}) {
270
323
  const instance = new FetchClient(origin ?? this.origin, {}, this.serializedSignal, this.ErrorCls);
324
+ if (ws) instance.configureWs(ws);
325
+ if (retry !== undefined) instance.http.setRetry(retry);
326
+ instance.#foreign = this.#foreign || (!!origin && !FetchClient.#sameOrigin(origin, this.origin));
271
327
  instance.#clientIp =
272
328
  clientIp !== undefined ? clientIp : FetchClient.#testClientIps ? FetchClient.#nextTestClientIp() : this.#clientIp;
273
329
  Object.entries(this.handler).forEach(([key, handler]) => {
@@ -277,6 +333,12 @@ export class FetchClient {
277
333
  if (connect) instance.connect();
278
334
  return FetchClient.#makeProxy(instance);
279
335
  }
336
+ /** Every socket this client holds or opens later: its own and one per `FetchPolicy.origin`. */
337
+ configureWs(options: WsClientOptions) {
338
+ this.#wsOptions = { ...this.#wsOptions, ...options };
339
+ this.ws.configure(options);
340
+ for (const ws of this.#originWs.values()) ws.configure(options);
341
+ }
280
342
  setJwt(jwt: string | null) {
281
343
  this.jwt = jwt;
282
344
  this.ws.setJwt(jwt);
@@ -289,19 +351,19 @@ export class FetchClient {
289
351
  if (target === this.origin.replace(/\/+$/, "")) return this.ws;
290
352
  const cached = this.#originWs.get(target);
291
353
  if (cached) return cached;
292
- const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls);
354
+ const ws = new WsClient(FetchClient.#makeWsUri(target), this.ErrorCls).configure(this.#wsOptions);
293
355
  this.#originWs.set(target, ws);
294
356
  ws.setJwt(this.jwt);
295
357
  ws.connect();
296
358
  return ws;
297
359
  }
298
360
  #makeRequestHeaders(option?: FetchPolicy): Record<string, string> {
299
- const clientIp = this.#clientIp ? { "x-real-ip": this.#clientIp } : FetchClient.#forwardedClientIp(option);
361
+ const clientIp = this.#clientIp ? { "x-real-ip": this.#clientIp } : this.#forwardedClientIp(option);
300
362
  return { ...this.#makeAuthHeaders(option), ...clientIp };
301
363
  }
302
364
 
303
- static #forwardedClientIp(option?: FetchPolicy): Record<string, string> {
304
- if (option?.origin) return {};
365
+ #forwardedClientIp(option?: FetchPolicy): Record<string, string> {
366
+ if (!this.#forwardsVisitor(option)) return {};
305
367
  try {
306
368
  if (getEnv().side !== "server") return {};
307
369
  const clientIp = requestHeaders().get("x-real-ip");
@@ -310,10 +372,17 @@ export class FetchClient {
310
372
  return {};
311
373
  }
312
374
  }
375
+ static #sameOrigin(a: string, b: string) {
376
+ return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
377
+ }
378
+
379
+ #forwardsVisitor(option?: FetchPolicy) {
380
+ return !this.#foreign && (!option?.origin || FetchClient.#sameOrigin(option.origin, this.origin));
381
+ }
313
382
  #makeAuthHeaders(option?: FetchPolicy): Record<string, string> {
314
383
  if (option?.token) return { Authorization: `Bearer ${option.token}` };
315
384
  try {
316
- if (getEnv().side === "server") {
385
+ if (getEnv().side === "server" && this.#forwardsVisitor(option)) {
317
386
  const authorization = requestHeaders().get("authorization");
318
387
  if (authorization) return { Authorization: authorization };
319
388
  const token = readAuthToken((key) => requestCookies().get(key)?.value);
@@ -344,11 +413,21 @@ export class FetchClient {
344
413
  case "query":
345
414
  return async (...argData: unknown[]) => {
346
415
  const { option, argMap, url } = requestOf(argData);
416
+ const bridge = option?.origin ? null : globalWithFetchBridge[FETCH_BRIDGE_KEY];
417
+ if (bridge) {
418
+ const value = await this.#bridged(bridge, {
419
+ key,
420
+ type: "query",
421
+ args: Object.fromEntries(argMap),
422
+ returns: endpoint.returns,
423
+ });
424
+ return parseReturn(value, { crystalize: option?.crystalize ?? true });
425
+ }
347
426
  const headers = this.#makeRequestHeaders(option);
348
427
  const baseUrl = this.#baseUrlOf(endpoint, option?.origin);
349
428
  const timeout = option?.timeout ?? endpoint.timeout;
350
429
 
351
- const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout });
430
+ const requestQuery = () => this.http.get(url, { headers, baseUrl, timeout, retry: option?.retry });
352
431
 
353
432
  const claim = baseUrl
354
433
  ? { value: requestQuery(), owned: true }
@@ -361,11 +440,23 @@ export class FetchClient {
361
440
  case "mutation":
362
441
  return async (...argData: unknown[]) => {
363
442
  const { option, argMap, url } = requestOf(argData);
443
+ const bridge = option?.origin ? null : globalWithFetchBridge[FETCH_BRIDGE_KEY];
444
+ if (bridge) {
445
+ const value = await this.#bridged(bridge, {
446
+ key,
447
+ type: "mutation",
448
+ args: Object.fromEntries(argMap),
449
+ returns: endpoint.returns,
450
+ });
451
+ return parseReturn(value, { crystalize: option?.crystalize ?? true });
452
+ }
364
453
  const body = HttpClient.makeBody(bodyArgs, uploadArgs, argMap);
365
454
  const response = await this.http.send(endpoint.method ?? "POST", url, body, {
366
455
  headers: this.#makeRequestHeaders(option),
367
456
  baseUrl: this.#baseUrlOf(endpoint, option?.origin),
368
457
  timeout: option?.timeout ?? endpoint.timeout,
458
+ retry: option?.retry,
459
+ idempotencyKey: option?.idempotencyKey,
369
460
  });
370
461
  return parseReturn(response, { crystalize: option?.crystalize ?? true });
371
462
  };
@@ -387,6 +478,8 @@ export class FetchClient {
387
478
  const parseReturn = this.#makeReturnParser(endpoint.returns);
388
479
  const wrappedListeners = new WeakMap<(data: unknown) => void, (data: unknown) => void>();
389
480
  return (...argData: unknown[]) => {
481
+
482
+ if (FetchClient.bridged) return () => undefined;
390
483
  const args = argData.slice(0, roomArgLength);
391
484
  const handleEvent = argData[roomArgLength] as (data: unknown) => void;
392
485
  const fetchPolicy = argData[roomArgLength + 1] as FetchPolicy | undefined;
@@ -416,6 +509,7 @@ export class FetchClient {
416
509
  const msgArgLength = msgArgs.length;
417
510
  const serializerMap = this.#makeArgSerializer(endpoint.args);
418
511
  return (...argData: unknown[]) => {
512
+ if (FetchClient.bridged) return;
419
513
  const args = argData.slice(0, msgArgLength);
420
514
  const fetchPolicy = argData[msgArgLength] as FetchPolicy | undefined;
421
515
  const data = msgArgs.map((arg, idx) => serializerMap.get(arg.name)?.(args[idx]) ?? null);
@@ -443,6 +537,15 @@ export class FetchClient {
443
537
  break;
444
538
  }
445
539
  }
540
+
541
+ async #bridged(bridge: FetchBridge, request: FetchBridgeCall) {
542
+ try {
543
+ return await bridge.call(request);
544
+ } catch (error) {
545
+ const payload = (error as RestoredError | null)?.toJSON?.();
546
+ throw payload && this.ErrorCls ? this.ErrorCls.fromJSON(payload) : error;
547
+ }
548
+ }
446
549
  static #makeWsUri(origin: string) {
447
550
  return `${origin.replace("http://", "ws://").replace("https://", "wss://")}${getWsPrefix()}`;
448
551
  }
@@ -817,7 +920,7 @@ export class FetchClient {
817
920
  );
818
921
  } else {
819
922
  Object.assign(handler, signal.handler);
820
- Object.entries(signal.serializedSignal).forEach(([refName, signal]) => {
923
+ Object.entries(signal.ownSerializedSignal ?? signal.serializedSignal).forEach(([refName, signal]) => {
821
924
  FetchClient.#mergeSerializedSignalInto(serializedSignal, refName, signal);
822
925
  });
823
926
  }
@@ -882,6 +985,7 @@ export class FetchClient {
882
985
  get(target, prop) {
883
986
  if (prop in target) return (target as any)[prop];
884
987
  else if (prop === "instance") return instance;
988
+ else if (prop === "ownSerializedSignal") return instance.ownSerializedSignal;
885
989
  else if (typeof prop === "string") {
886
990
  const handler = instance.#getOrCreateHandler(prop);
887
991
  if (handler) return handler;
@@ -1,3 +1,4 @@
1
+ import { Backoff, type RetryPolicy } from "akanjs/common";
1
2
  import type { HttpMutationMethod, SerializedArg } from "akanjs/signal";
2
3
  import { type ErrorConstructor, type RestoredError, restoreRemoteError } from "./remoteError";
3
4
 
@@ -10,6 +11,9 @@ export interface HttpClientOptions {
10
11
 
11
12
  interface FetchOptions extends Pick<HttpClientOptions, "headers" | "timeout"> {
12
13
  baseUrl?: string;
14
+ /** Over the client's default; `false` sends once. */
15
+ retry?: RetryPolicy | false;
16
+ idempotencyKey?: string;
13
17
  }
14
18
 
15
19
  const jsonContentType = /^application\/(?:[\w.+-]+\+)?json\b/i;
@@ -23,6 +27,14 @@ const transportErrorKeyMap = {
23
27
  } as const;
24
28
 
25
29
  const serverUnreachableKey = "base.error.serverUnreachable";
30
+
31
+ const retryableKeys = new Set([
32
+ serverUnreachableKey,
33
+ "base.error.serverUnavailable",
34
+ "base.error.gatewayTimeout",
35
+ "base.error.tooManyRequests",
36
+ "base.error.idempotencyInFlight",
37
+ ]);
26
38
  const unexpectedResponseKey = "base.error.unexpectedResponse";
27
39
  const transportDetailLimit = 200;
28
40
 
@@ -32,6 +44,7 @@ export class HttpClient {
32
44
  readonly baseUrl: string;
33
45
  #headers: Record<string, string>;
34
46
  #timeout?: number | false;
47
+ #retry: RetryPolicy | false = false;
35
48
  private ErrorCls?: ErrorConstructor;
36
49
  constructor(baseUrl: string, options: HttpClientOptions = {}) {
37
50
  this.baseUrl = baseUrl;
@@ -47,6 +60,10 @@ export class HttpClient {
47
60
  setTimeout(timeout?: number | false) {
48
61
  this.#timeout = timeout;
49
62
  }
63
+ /** What every call that names no `retry` of its own does; `false`, the default, sends once. */
64
+ setRetry(retry: RetryPolicy | false) {
65
+ this.#retry = retry;
66
+ }
50
67
  #resolveUrl(url: string, options: FetchOptions) {
51
68
  return `${(options.baseUrl ?? this.baseUrl).replace(/\/$/, "")}${url}`;
52
69
  }
@@ -102,6 +119,34 @@ export class HttpClient {
102
119
  }
103
120
 
104
121
  async #request<Returns>(url: string, init: RequestInit, options: FetchOptions = {}): Promise<Returns> {
122
+ const retry = options.retry ?? this.#retry;
123
+ const mutation = !!init.method && init.method !== "GET";
124
+ const idempotencyKey = options.idempotencyKey ?? (retry && mutation ? crypto.randomUUID() : undefined);
125
+ const sent: RequestInit = idempotencyKey
126
+ ? { ...init, headers: { ...(init.headers as Record<string, string>), "Idempotency-Key": idempotencyKey } }
127
+ : init;
128
+ const policy = retry && !(init.body instanceof FormData) ? retry : null;
129
+ for (let attempt = 1; ; attempt++) {
130
+ try {
131
+ return await this.#requestOnce<Returns>(url, sent, options);
132
+ } catch (error) {
133
+ const wait = policy ? HttpClient.retryWait(error, attempt, policy) : null;
134
+ if (wait === null) throw error;
135
+ await new Promise((resolve) => setTimeout(resolve, wait));
136
+ }
137
+ }
138
+ }
139
+
140
+ /** The wait before the next try, or `null` when this error is final or the tries are spent. */
141
+ static retryWait(error: unknown, attempt: number, { attempts = 4, ...backoff }: RetryPolicy): number | null {
142
+ const restored = error as RestoredError | undefined;
143
+ if (attempt >= attempts || !retryableKeys.has(restored?.error ?? "")) return null;
144
+ const wait = Backoff.delay(attempt, { min: 500, ...backoff });
145
+ const seconds = (restored?.data as { seconds?: unknown } | undefined)?.seconds;
146
+ return typeof seconds === "number" && seconds > 0 ? Math.max(wait, seconds * 1000) : wait;
147
+ }
148
+
149
+ async #requestOnce<Returns>(url: string, init: RequestInit, options: FetchOptions): Promise<Returns> {
105
150
  const res = await this.#fetch(url, this.#withTimeout(init, options));
106
151
  const body = await this.#readBody(res);
107
152
  if (res.ok) return body as Returns;
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Backoff,
2
3
  Logger,
3
4
  type WebsocketHeartbeatAckData,
4
5
  websocketAuthContract,
@@ -17,17 +18,40 @@ import { websocketRoomContract } from "../../common/websocketContract";
17
18
  import { type ErrorConstructor, restoreRemoteError } from "./remoteError";
18
19
 
19
20
  export interface WsClientReconnectOptions {
20
- enabled?: boolean;
21
- interval?: number;
21
+ /** The first wait in ms; each attempt in a row doubles it. */
22
+ min?: number;
23
+ max?: number;
24
+ /** The share of each wait drawn at random, 0 to 1, so clients one deploy dropped together come back apart. */
25
+ jitter?: number;
26
+ /** Attempts in a row before the client gives up; unset, it never does. */
22
27
  maxAttempts?: number;
23
28
  }
24
29
 
30
+ export type WsClientState = "connecting" | "open" | "closed" | "gaveUp";
31
+
32
+ export interface WsClientOptions {
33
+ /** `false` stays closed after a drop. */
34
+ reconnect?: WsClientReconnectOptions | false;
35
+ /** Sent with the upgrade by a server-side client, the token among them as `authorization`; a browser sends none. */
36
+ headers?: Record<string, string>;
37
+ tls?: { rejectUnauthorized?: boolean; ca?: string | string[] };
38
+ onStateChange?: (state: WsClientState) => void;
39
+ /** A room the server took back when the credential changed. */
40
+ onRevoked?: (roomId: string) => void;
41
+ /** A frame the server answered with an error, and the subscribe or message key it answered. */
42
+ onError?: (error: unknown, key?: string) => void;
43
+ }
44
+
25
45
  interface SubscribeOption {
26
46
  key: string;
27
47
  data: unknown[];
28
48
  listener: Set<(data: unknown) => void>;
29
- /** After a resubscribe following a drop: what was published meanwhile is gone, so a room can only say so. */
49
+ /** After a resubscribe following a drop, or a hole in the frames: what was published meanwhile is gone. */
30
50
  resync: Set<() => void>;
51
+ /** The numbering of the frames the room delivered, while the server numbers them. */
52
+ cursor: { epoch: string | null; seq: number | null };
53
+ /** Resubscribed after a drop: the ack says whether what was missed came back. */
54
+ recovering: boolean;
31
55
  }
32
56
  interface Listener {
33
57
  callback: (data: unknown) => void;
@@ -68,6 +92,8 @@ export class WsClient {
68
92
  #jwt: string | null = null;
69
93
  #heartbeatTimer: ReturnType<typeof setInterval> | null = null;
70
94
  #lastInboundAt = 0;
95
+ #options: WsClientOptions = {};
96
+ #state: WsClientState = "closed";
71
97
  connected = false;
72
98
 
73
99
  constructor(
@@ -81,6 +107,26 @@ export class WsClient {
81
107
  this.ErrorCls = ErrorCls;
82
108
  }
83
109
 
110
+ /** Merged over what was set before; read at the next connect and the next drop. */
111
+ configure(options: WsClientOptions) {
112
+ this.#options = { ...this.#options, ...options };
113
+ return this;
114
+ }
115
+
116
+ get state(): WsClientState {
117
+ return this.#state;
118
+ }
119
+
120
+ #setState(state: WsClientState) {
121
+ if (this.#state === state) return;
122
+ this.#state = state;
123
+ this.#options.onStateChange?.(state);
124
+ }
125
+
126
+ static reconnectDelay(attempt: number, policy: WsClientReconnectOptions = {}) {
127
+ return Backoff.delay(attempt, policy);
128
+ }
129
+
84
130
  /**
85
131
  * For clients holding the token in memory (native, cross-origin): the handshake carries only a same-origin cookie.
86
132
  * `null` drops that cookie server-side and revokes the rooms it authorized.
@@ -107,8 +153,9 @@ export class WsClient {
107
153
  #connect() {
108
154
  if (this.#destroyed) return;
109
155
 
156
+ this.#setState("connecting");
110
157
  try {
111
- this.#ws = new WebSocket(this.url);
158
+ this.#ws = this.#open();
112
159
  } catch (error) {
113
160
  this.logger.error(`Cannot open a WebSocket to ${this.url}: ${error instanceof Error ? error.message : error}`);
114
161
  return;
@@ -118,16 +165,25 @@ export class WsClient {
118
165
  this.#reconnectAttempts = 0;
119
166
  this.connected = true;
120
167
  this.logger.debug(`WebSocket connected`);
168
+ this.#setState("open");
121
169
  this.#startHeartbeat();
122
170
 
123
171
  if (this.#jwt) this.#sendAuth();
124
172
  const reconnected = this.#hadConnection;
125
173
  this.#hadConnection = true;
126
174
  this.#roomSubscribeMap.forEach((option) => {
127
- const data: WebsocketReqData = { key: option.key, data: option.data, subscribe: true };
175
+ const { epoch, seq } = option.cursor;
176
+ const since = reconnected && epoch !== null && seq !== null ? { epoch, seq } : undefined;
177
+ const data: WebsocketReqData = {
178
+ key: option.key,
179
+ data: option.data,
180
+ subscribe: true,
181
+ ...(since ? { since } : {}),
182
+ };
128
183
  this.#ws?.send(JSON.stringify(data));
129
- if (!reconnected) return;
130
- for (const handler of option.resync) handler();
184
+
185
+ option.cursor = { epoch: null, seq: null };
186
+ option.recovering = reconnected;
131
187
  });
132
188
  const queued = this.#outbox;
133
189
  this.#outbox = [];
@@ -142,8 +198,12 @@ export class WsClient {
142
198
  else this.logger.warn("Unknown binary WebSocket frame");
143
199
  return;
144
200
  }
145
- const parsed = JSON.parse(e.data) as { error?: unknown } & WebsocketResData;
146
- if (parsed?.error) throw restoreRemoteError(parsed, 500, this.ErrorCls);
201
+ const parsed = JSON.parse(e.data) as { error?: unknown; key?: string } & WebsocketResData;
202
+ if (parsed?.error) {
203
+ const error = restoreRemoteError(parsed, 500, this.ErrorCls);
204
+ this.#options.onError?.(error, parsed.key);
205
+ throw error;
206
+ }
147
207
  const type = (parsed as WebsocketResData | WebsocketHeartbeatAckData).type;
148
208
  switch (type) {
149
209
  case "msg": {
@@ -158,11 +218,12 @@ export class WsClient {
158
218
  else this.#roomAliasMap.delete(sub.roomId);
159
219
  }
160
220
  this.logger.verbose(`Websocket ${sub.subscribe ? "subscribe" : "unsubscribe"} accepted: ${sub.roomId}`);
221
+ if (sub.subscribe) this.#acknowledge(sub);
161
222
  break;
162
223
  }
163
224
  case "pub": {
164
225
  const publishData = parsed as WebsocketPublishData;
165
- this.#handlePubsub(publishData.roomId, publishData.data);
226
+ this.#handlePubsub(publishData.roomId, publishData.data, publishData.seq);
166
227
  break;
167
228
  }
168
229
  case "pong":
@@ -170,9 +231,11 @@ export class WsClient {
170
231
  case "auth": {
171
232
  const ack = parsed as WebsocketAuthAck;
172
233
  for (const roomId of ack.revokedRooms) {
173
- this.#roomSubscribeMap.delete(this.#roomAliasMap.get(roomId) ?? roomId);
234
+ const requestRoomId = this.#roomAliasMap.get(roomId) ?? roomId;
235
+ this.#roomSubscribeMap.delete(requestRoomId);
174
236
  this.#roomAliasMap.delete(roomId);
175
237
  this.logger.warn(`Websocket room ${roomId} is no longer authorized`);
238
+ this.#options.onRevoked?.(requestRoomId);
176
239
  }
177
240
  break;
178
241
  }
@@ -193,6 +256,7 @@ export class WsClient {
193
256
  this.logger.debug(`WebSocket closed: ${event.code} ${event.reason}`);
194
257
  this.connected = false;
195
258
  this.#stopHeartbeat();
259
+ if (!this.#destroyed) this.#setState("closed");
196
260
  this.#scheduleReconnect();
197
261
  };
198
262
  }
@@ -225,15 +289,46 @@ export class WsClient {
225
289
 
226
290
  #scheduleReconnect() {
227
291
  if (this.#destroyed || !this.#ws) return;
228
- const interval = 3000;
292
+ this.#ws = null;
293
+ const { reconnect } = this.#options;
229
294
  this.#reconnectAttempts += 1;
295
+ if (reconnect === false || this.#reconnectAttempts > (reconnect?.maxAttempts ?? Number.POSITIVE_INFINITY)) {
296
+ this.logger.warn(`WebSocket to ${this.url} gave up after ${this.#reconnectAttempts - 1} attempt(s)`);
297
+ this.#setState("gaveUp");
298
+ return;
299
+ }
300
+ const interval = WsClient.reconnectDelay(this.#reconnectAttempts, reconnect || undefined);
230
301
  this.logger.debug(`WebSocket reconnecting in ${interval}ms (attempt ${this.#reconnectAttempts})`);
231
-
232
- this.#ws = null;
233
302
  this.#reconnectTimer = setTimeout(() => {
234
303
  this.#reconnectTimer = null;
235
304
  this.#connect();
236
305
  }, interval);
306
+
307
+ this.#reconnectTimer.unref?.();
308
+ }
309
+
310
+ #open(): WebSocket {
311
+ if (typeof window !== "undefined") return new WebSocket(this.url);
312
+ const { headers, tls } = this.#options;
313
+ const authorization: Record<string, string> = this.#jwt ? { authorization: `Bearer ${this.#jwt}` } : {};
314
+ const BunWebSocket = WebSocket as unknown as new (
315
+ url: string,
316
+ options: { headers?: Record<string, string>; tls?: WsClientOptions["tls"] },
317
+ ) => WebSocket;
318
+ return new BunWebSocket(this.url, { headers: { ...headers, ...authorization }, ...(tls ? { tls } : {}) });
319
+ }
320
+
321
+ #acknowledge(ack: WebsocketSubscribeAck) {
322
+ const option = this.#roomSubscribeMap.get(ack.requestRoomId);
323
+ if (!option) return;
324
+ if (ack.cursor) {
325
+ option.cursor.epoch = ack.cursor.epoch;
326
+ if (option.cursor.seq === null || ack.cursor.seq > option.cursor.seq) option.cursor.seq = ack.cursor.seq;
327
+ }
328
+ if (!option.recovering) return;
329
+ option.recovering = false;
330
+ if (ack.replayed === true) return;
331
+ for (const handler of option.resync) handler();
237
332
  }
238
333
  #handleMessage(key: string, data: unknown) {
239
334
  const listenerSet = this.#listenerMap.get(key);
@@ -246,10 +341,16 @@ export class WsClient {
246
341
  }
247
342
  }
248
343
  }
249
- #handlePubsub(roomId: string, data: unknown) {
344
+ #handlePubsub(roomId: string, data: unknown, seq?: number) {
250
345
  const roomSubscribe = this.#roomSubscribeMap.get(this.#roomAliasMap.get(roomId) ?? roomId);
251
346
  if (!roomSubscribe) return;
347
+ const last = roomSubscribe.cursor.seq;
348
+
349
+ if (seq !== undefined && last !== null && seq <= last) return;
350
+ if (seq !== undefined) roomSubscribe.cursor.seq = seq;
252
351
  for (const listener of roomSubscribe.listener) listener(data);
352
+
353
+ if (seq !== undefined && last !== null && seq > last + 1) for (const handler of roomSubscribe.resync) handler();
253
354
  }
254
355
 
255
356
  destroy() {
@@ -267,6 +368,7 @@ export class WsClient {
267
368
  this.#roomAliasMap.clear();
268
369
  this.#ws?.close();
269
370
  this.#ws = null;
371
+ this.#setState("closed");
270
372
  }
271
373
 
272
374
  on<Data = unknown>(key: string, callback: (data: Data) => void) {
@@ -329,7 +431,14 @@ export class WsClient {
329
431
  if (!this.#ws) this.#warnUnconnected("subscribe", option.key);
330
432
  let roomSubscribe = this.#roomSubscribeMap.get(roomId);
331
433
  if (!roomSubscribe) {
332
- roomSubscribe = { key: option.key, data: option.data, listener: new Set(), resync: new Set() };
434
+ roomSubscribe = {
435
+ key: option.key,
436
+ data: option.data,
437
+ listener: new Set(),
438
+ resync: new Set(),
439
+ cursor: { epoch: null, seq: null },
440
+ recovering: false,
441
+ };
333
442
  this.#roomSubscribeMap.set(roomId, roomSubscribe);
334
443
  if (this.#ws?.readyState === WebSocket.OPEN) this.#sendSubscribe(option.key, option.data, true);
335
444
  this.logger.verbose(`Websocket subscribe pubsub for ${roomId}`);