akanjs 3.1.1 → 3.1.2-rc.0

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 (333) hide show
  1. package/README.ko.md +22 -0
  2. package/README.md +22 -0
  3. package/client/cookie.ts +23 -7
  4. package/client/csrTypes.ts +1 -1
  5. package/client/makePageProto.tsx +10 -1
  6. package/client/router.ts +57 -16
  7. package/common/IpLiteral.ts +4 -2
  8. package/common/TrustedProxy.ts +45 -2
  9. package/common/akanNames.ts +169 -0
  10. package/common/codeAgentProfile.ts +51 -4
  11. package/common/index.ts +13 -0
  12. package/common/localeConfig.ts +30 -5
  13. package/common/localePath.ts +26 -0
  14. package/common/markdownSpans.ts +3 -1
  15. package/common/mcpExposure.ts +7 -7
  16. package/common/websocketContract.ts +50 -0
  17. package/constant/index.ts +1 -0
  18. package/constant/inputParser.ts +10 -2
  19. package/constant/requestValue.ts +47 -0
  20. package/dictionary/agent.dictionary.ts +2 -2
  21. package/dictionary/base.dictionary.ts +4 -0
  22. package/dictionary/dictInfo.ts +9 -9
  23. package/document/database.ts +1 -0
  24. package/document/documentQuery.ts +42 -6
  25. package/document/filterMeta.ts +56 -8
  26. package/document/queryEvaluator.ts +13 -8
  27. package/fetch/client/fetchClient.ts +28 -62
  28. package/fetch/client/httpClient.ts +1 -0
  29. package/fetch/client/sliceInitHandle.ts +18 -18
  30. package/fetch/index.ts +1 -0
  31. package/fetch/sessionCookie.ts +37 -0
  32. package/index.ts +8 -0
  33. package/package.json +13 -27
  34. package/server/akanApp.ts +108 -29
  35. package/server/akanServer.ts +18 -7
  36. package/server/assetHeaders.ts +20 -1
  37. package/server/cachePolicy.ts +18 -5
  38. package/server/devtools/depsSerializer.ts +2 -1
  39. package/server/devtools/devtoolsRouter.ts +3 -2
  40. package/server/devtools/signalSerializer.ts +5 -5
  41. package/server/hmr/clientReport.ts +10 -5
  42. package/server/imageOptimizer.ts +117 -22
  43. package/server/lifecycle/buildEnv.ts +11 -0
  44. package/server/lifecycle/loadBuildEnv.ts +3 -0
  45. package/server/logging/logControlSocket.ts +20 -14
  46. package/server/logging/logStreamRoute.ts +12 -8
  47. package/server/logging/rotatingLogWriter.ts +15 -1
  48. package/server/machine/machineToken.ts +4 -0
  49. package/server/mcp/McpAuth.ts +12 -4
  50. package/server/mcp/McpExecutionContext.ts +4 -0
  51. package/server/mcp/McpRouter.ts +25 -13
  52. package/server/mcp/PagePromptComposer.ts +4 -7
  53. package/server/oauth/OAuthClientIdMetadata.ts +29 -6
  54. package/server/oauth/OAuthRegistration.ts +9 -0
  55. package/server/ops/failureLimiter.ts +42 -0
  56. package/server/ops/opsCommand.ts +19 -7
  57. package/server/ops/opsRoute.ts +60 -9
  58. package/server/ops/opsToken.ts +5 -0
  59. package/server/ops/snapshotJobs.ts +3 -1
  60. package/server/ops/snapshotRestore.ts +23 -1
  61. package/server/ops/sqliteSnapshot.ts +98 -18
  62. package/server/ops/statusAccess.ts +37 -19
  63. package/server/proxy/hostBasePathWebProxy.ts +4 -7
  64. package/server/proxy/localeWebProxy.ts +36 -16
  65. package/server/resolver/database.resolver.ts +15 -11
  66. package/server/resolver/service.resolver.ts +17 -2
  67. package/server/resolver/signal.resolver.ts +50 -40
  68. package/server/routeElementComposer.tsx +42 -9
  69. package/server/routeShellGate.ts +42 -0
  70. package/server/routing/apiRouter.ts +64 -1
  71. package/server/routing/serverLimits.ts +151 -0
  72. package/server/routing/soloAppRoutes.ts +23 -12
  73. package/server/rscClient.tsx +5 -3
  74. package/server/rscScrollRestoration.ts +60 -9
  75. package/server/rscWorker.tsx +76 -24
  76. package/server/sitemap.ts +5 -7
  77. package/server/ssrFromRscRenderer.tsx +36 -4
  78. package/server/ssrTypes.ts +5 -2
  79. package/server/staticPath.ts +36 -1
  80. package/server/systemPageDocument.tsx +5 -3
  81. package/server/systemPages.tsx +14 -8
  82. package/server/tunnel/TunnelAgent.ts +19 -2
  83. package/server/tunnel/TunnelDataSocket.ts +4 -2
  84. package/server/tunnel/TunnelHttpStream.ts +7 -2
  85. package/server/tunnel/TunnelTcpStream.ts +6 -3
  86. package/server/tunnel/TunnelWebsocketStream.ts +9 -2
  87. package/server/tunnel/tunnelStream.ts +6 -0
  88. package/server/types.tsx +20 -2
  89. package/server/webRouter.ts +38 -18
  90. package/service/agent.service.ts +9 -7
  91. package/service/injectInfo.ts +2 -1
  92. package/service/liveSyncHub.ts +6 -0
  93. package/service/predefinedAdaptor/anthropicLlm.ts +13 -12
  94. package/service/predefinedAdaptor/database.adaptor.ts +4 -0
  95. package/service/predefinedAdaptor/llm.adaptor.ts +3 -0
  96. package/service/predefinedAdaptor/llmOverflow.ts +5 -1
  97. package/service/predefinedAdaptor/openaiDialect.ts +9 -8
  98. package/service/predefinedAdaptor/searchIndex.ts +21 -1
  99. package/service/predefinedAdaptor/sql/QueryCompiler.ts +28 -4
  100. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +45 -11
  101. package/service/predefinedAdaptor/sql/dialect/postgres.ts +6 -1
  102. package/service/predefinedAdaptor/sql/types.ts +3 -0
  103. package/service/predefinedAdaptor/storage.adaptor.ts +11 -10
  104. package/service/serve.ts +3 -3
  105. package/service/serviceModule.ts +28 -24
  106. package/service/types.ts +3 -0
  107. package/signal/SignalFailure.ts +5 -2
  108. package/signal/agent/AgentCatalogue.ts +3 -2
  109. package/signal/agentMeter.ts +81 -9
  110. package/signal/agentRelayPolicy.ts +53 -2
  111. package/signal/agentTurnStream.ts +12 -1
  112. package/signal/guard.ts +2 -2
  113. package/signal/mcp/McpDocument.ts +3 -3
  114. package/signal/mcp/McpUriTemplate.ts +5 -5
  115. package/signal/middleware.ts +15 -1
  116. package/signal/roomKey.ts +53 -0
  117. package/signal/signalContext.ts +100 -40
  118. package/signal/slice.ts +27 -22
  119. package/signal/trace.ts +17 -6
  120. package/store/action.ts +20 -49
  121. package/store/actionTag.ts +2 -0
  122. package/store/agent/ScreenReader.ts +36 -4
  123. package/store/agent/ScreenTarget.ts +1 -1
  124. package/store/agent/StoreCatalogue.ts +7 -1
  125. package/store/agent/StoreSurfaceSource.ts +5 -0
  126. package/store/agentic/AgentConfirm.ts +30 -0
  127. package/store/agentic/StStateBuilder.ts +2 -2
  128. package/store/agentic/StToolBuilder.ts +7 -2
  129. package/store/agentic/formFields.ts +1 -2
  130. package/store/agentic/index.ts +1 -0
  131. package/store/agentic/useFieldTool.ts +4 -3
  132. package/store/deprecatedNames.ts +6 -0
  133. package/store/draftStore.ts +2 -2
  134. package/store/formUploadOf.ts +6 -5
  135. package/store/index.ts +1 -2
  136. package/store/sliceKeys.ts +9 -32
  137. package/store/state.ts +1 -1
  138. package/store/stateBuilder.ts +1 -1
  139. package/store/store.ts +1 -1
  140. package/store/storeInstance.ts +12 -8
  141. package/store/storeRegistry.ts +14 -2
  142. package/types/client/cookie.d.ts +7 -2
  143. package/types/client/csrTypes.d.ts +1 -1
  144. package/types/client/router.d.ts +4 -1
  145. package/types/common/akanNames.d.ts +149 -0
  146. package/types/common/codeAgentProfile.d.ts +4 -1
  147. package/types/common/index.d.ts +3 -1
  148. package/types/common/localeConfig.d.ts +8 -0
  149. package/types/common/localePath.d.ts +13 -0
  150. package/types/common/websocketContract.d.ts +7 -0
  151. package/types/constant/index.d.ts +1 -0
  152. package/types/constant/requestValue.d.ts +5 -0
  153. package/types/dictionary/base.dictionary.d.ts +1 -1
  154. package/types/dictionary/dictionary.d.ts +3 -3
  155. package/types/document/database.d.ts +1 -0
  156. package/types/document/documentQuery.d.ts +3 -0
  157. package/types/document/filterMeta.d.ts +4 -0
  158. package/types/fetch/client/sliceInitHandle.d.ts +1 -2
  159. package/types/fetch/index.d.ts +1 -0
  160. package/types/fetch/sessionCookie.d.ts +12 -0
  161. package/types/index.d.ts +8 -0
  162. package/types/server/akanApp.d.ts +1 -0
  163. package/types/server/assetHeaders.d.ts +5 -0
  164. package/types/server/cachePolicy.d.ts +2 -0
  165. package/types/server/devtools/devtoolsRouter.d.ts +1 -1
  166. package/types/server/imageOptimizer.d.ts +4 -1
  167. package/types/server/lifecycle/buildEnv.d.ts +2 -0
  168. package/types/server/lifecycle/loadBuildEnv.d.ts +1 -0
  169. package/types/server/logging/logStreamRoute.d.ts +1 -1
  170. package/types/server/logging/rotatingLogWriter.d.ts +3 -0
  171. package/types/server/machine/machineToken.d.ts +2 -0
  172. package/types/server/mcp/McpAuth.d.ts +2 -1
  173. package/types/server/mcp/McpExecutionContext.d.ts +1 -0
  174. package/types/server/oauth/OAuthRegistration.d.ts +2 -0
  175. package/types/server/ops/failureLimiter.d.ts +11 -0
  176. package/types/server/ops/opsRoute.d.ts +4 -2
  177. package/types/server/ops/opsToken.d.ts +2 -0
  178. package/types/server/ops/snapshotRestore.d.ts +3 -1
  179. package/types/server/ops/sqliteSnapshot.d.ts +7 -1
  180. package/types/server/ops/statusAccess.d.ts +8 -4
  181. package/types/server/proxy/hostBasePathWebProxy.d.ts +1 -0
  182. package/types/server/resolver/signal.resolver.d.ts +0 -1
  183. package/types/server/routeElementComposer.d.ts +10 -4
  184. package/types/server/routeShellGate.d.ts +12 -0
  185. package/types/server/routing/apiRouter.d.ts +6 -1
  186. package/types/server/routing/serverLimits.d.ts +23 -0
  187. package/types/server/routing/soloAppRoutes.d.ts +1 -1
  188. package/types/server/rscScrollRestoration.d.ts +1 -0
  189. package/types/server/ssrTypes.d.ts +5 -2
  190. package/types/server/staticPath.d.ts +1 -0
  191. package/types/server/systemPageDocument.d.ts +1 -0
  192. package/types/server/tunnel/TunnelAgent.d.ts +6 -0
  193. package/types/server/tunnel/TunnelDataSocket.d.ts +2 -0
  194. package/types/server/tunnel/TunnelTcpStream.d.ts +1 -1
  195. package/types/server/tunnel/tunnelStream.d.ts +1 -0
  196. package/types/server/types.d.ts +11 -2
  197. package/types/service/agent.service.d.ts +1 -1
  198. package/types/service/liveSyncHub.d.ts +1 -0
  199. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +1 -1
  200. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +1 -0
  201. package/types/service/predefinedAdaptor/openaiDialect.d.ts +1 -1
  202. package/types/service/predefinedAdaptor/searchIndex.d.ts +5 -1
  203. package/types/service/predefinedAdaptor/sql/SqlDocumentStore.d.ts +1 -0
  204. package/types/service/predefinedAdaptor/sql/types.d.ts +3 -0
  205. package/types/service/types.d.ts +3 -0
  206. package/types/signal/SignalFailure.d.ts +1 -1
  207. package/types/signal/agentMeter.d.ts +4 -1
  208. package/types/signal/agentRelayPolicy.d.ts +12 -0
  209. package/types/signal/guard.d.ts +1 -1
  210. package/types/signal/roomKey.d.ts +8 -0
  211. package/types/signal/signalContext.d.ts +20 -6
  212. package/types/signal/slice.d.ts +1 -0
  213. package/types/signal/trace.d.ts +4 -0
  214. package/types/store/actionTag.d.ts +2 -0
  215. package/types/store/agent/ScreenReader.d.ts +2 -1
  216. package/types/store/agent/StoreCatalogue.d.ts +5 -0
  217. package/types/store/agentic/AgentConfirm.d.ts +15 -0
  218. package/types/store/agentic/StToolBuilder.d.ts +3 -1
  219. package/types/store/agentic/index.d.ts +1 -0
  220. package/types/store/deprecatedNames.d.ts +5 -0
  221. package/types/store/index.d.ts +1 -2
  222. package/types/store/sliceKeys.d.ts +12 -2
  223. package/types/ui/Agent/agentSessionOf.d.ts +2 -0
  224. package/types/ui/Agent/useAccountBoundSession.d.ts +2 -0
  225. package/types/ui/Agent/useScreenActions.d.ts +1 -1
  226. package/types/ui/Icon.d.ts +15 -0
  227. package/types/ui/animated.d.ts +3 -3
  228. package/types/ui/sliceNamesOf.d.ts +4 -4
  229. package/types/vendor/@akanjs/native/packages/cli/src/config.d.ts +10 -0
  230. package/types/vendor/@akanjs/native/packages/cli/src/lib/updates.d.ts +6 -0
  231. package/types/vendor/@akanjs/native/packages/core/src/kernel.d.ts +6 -1
  232. package/types/vendor/@akanjs/native/packages/desktop/src/paths.d.ts +7 -0
  233. package/types/vendor/@akanjs/native/plugins/single-instance/src/socket.d.ts +2 -0
  234. package/types/vendor/@akanjs/native/plugins/updates/src/desktop.d.ts +2 -0
  235. package/types/vendor/use-agentic/AgentSession.d.ts +6 -1
  236. package/types/vendor/use-agentic/ToolRunner.d.ts +7 -2
  237. package/types/vendor/use-agentic/types.d.ts +2 -0
  238. package/types/vendor/use-agentic/useAgentTool.d.ts +2 -1
  239. package/types/webkit/useCsrValues.d.ts +3 -3
  240. package/ui/Agent/Chat.tsx +2 -0
  241. package/ui/Agent/Zone.tsx +2 -0
  242. package/ui/Agent/agentSessionOf.ts +54 -5
  243. package/ui/Agent/useAccountBoundSession.ts +26 -0
  244. package/ui/Agent/useScreenActions.ts +1 -1
  245. package/ui/BottomSheet.tsx +1 -1
  246. package/ui/Button.tsx +5 -5
  247. package/ui/Data/Item.tsx +2 -8
  248. package/ui/Data/ListContainer.tsx +4 -4
  249. package/ui/Data/QueryMaker.tsx +3 -2
  250. package/ui/Data/RefPicker.tsx +3 -2
  251. package/ui/Data/UnlistedEditModal.tsx +2 -1
  252. package/ui/Data/ViewActions.tsx +4 -4
  253. package/ui/Data/dataExport.ts +3 -0
  254. package/ui/Data/useViewModal.ts +10 -15
  255. package/ui/Dialog/LegacyModal.tsx +1 -1
  256. package/ui/Dialog/Provider.tsx +2 -2
  257. package/ui/DragAction.tsx +1 -1
  258. package/ui/Dropdown.tsx +2 -2
  259. package/ui/Field/lightRefCache.ts +2 -2
  260. package/ui/Field/useRelationOptions.ts +3 -10
  261. package/ui/Icon.tsx +87 -0
  262. package/ui/KeyboardAvoiding.tsx +1 -1
  263. package/ui/Layout/Sider.tsx +3 -3
  264. package/ui/Link/Anchor.tsx +4 -1
  265. package/ui/Load/Edit.tsx +2 -2
  266. package/ui/Load/Edit_Client.tsx +3 -3
  267. package/ui/Load/Pagination.tsx +3 -3
  268. package/ui/Load/View.tsx +15 -14
  269. package/ui/Loading/ProgressBar.tsx +1 -1
  270. package/ui/Model/DraftBar.tsx +7 -14
  271. package/ui/Model/EditModal.tsx +9 -40
  272. package/ui/Model/EditWrapper.tsx +3 -5
  273. package/ui/Model/NewWrapper_Client.tsx +3 -7
  274. package/ui/Model/Remove.tsx +2 -2
  275. package/ui/Model/RemoveWrapper.tsx +2 -4
  276. package/ui/Model/SureToRemove.tsx +2 -2
  277. package/ui/Model/ViewEditModal.tsx +9 -20
  278. package/ui/Model/ViewModal.tsx +4 -11
  279. package/ui/Model/ViewWrapper.tsx +4 -6
  280. package/ui/ScreenNavigator.tsx +2 -2
  281. package/ui/Signal/WebSocket.tsx +2 -2
  282. package/ui/System/CSR.tsx +3 -3
  283. package/ui/System/Reconnect.tsx +2 -2
  284. package/ui/System/SSR.tsx +2 -2
  285. package/ui/System/SelectLanguage.tsx +1 -1
  286. package/ui/System/ThemeToggle.tsx +1 -1
  287. package/ui/Tab/Provider.tsx +1 -0
  288. package/ui/Toast.tsx +7 -15
  289. package/ui/index.ts +0 -1
  290. package/ui/sliceNamesOf.ts +25 -15
  291. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeActivity.kt +2 -1
  292. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeKernel.kt +10 -2
  293. package/vendor/@akanjs/native/native/android/src/com/akanjs/runtime/AkanNativeVectors.kt +4 -0
  294. package/vendor/@akanjs/native/native/desktop/src/acl.rs +166 -0
  295. package/vendor/@akanjs/native/native/desktop/src/lib.rs +25 -6
  296. package/vendor/@akanjs/native/native/desktop/src/navigation.rs +22 -10
  297. package/vendor/@akanjs/native/native/desktop/src/routes.rs +2 -2
  298. package/vendor/@akanjs/native/native/desktop/src/vectors.rs +28 -0
  299. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +1 -1
  300. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeKernel.swift +10 -2
  301. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeVectors.swift +4 -0
  302. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeViewController.swift +1 -0
  303. package/vendor/@akanjs/native/packages/cli/src/config.ts +10 -0
  304. package/vendor/@akanjs/native/packages/cli/src/lib/configkeys.ts +2 -0
  305. package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +5 -3
  306. package/vendor/@akanjs/native/packages/cli/src/lib/prepare.ts +4 -0
  307. package/vendor/@akanjs/native/packages/cli/src/lib/publish.ts +2 -0
  308. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +30 -0
  309. package/vendor/@akanjs/native/packages/cli/src/lib/vectors.ts +1 -1
  310. package/vendor/@akanjs/native/packages/core/src/kernel.ts +18 -2
  311. package/vendor/@akanjs/native/packages/core/vectors/navigation.json +22 -1
  312. package/vendor/@akanjs/native/packages/core/vectors/routes.json +4 -0
  313. package/vendor/@akanjs/native/packages/desktop/src/main.ts +5 -1
  314. package/vendor/@akanjs/native/packages/desktop/src/paths.ts +24 -1
  315. package/vendor/@akanjs/native/plugins/external-view/src/desktop.ts +4 -5
  316. package/vendor/@akanjs/native/plugins/file-picker/src/desktop.ts +6 -8
  317. package/vendor/@akanjs/native/plugins/filesystem/src/desktop.ts +4 -2
  318. package/vendor/@akanjs/native/plugins/single-instance/src/socket.ts +22 -3
  319. package/vendor/@akanjs/native/plugins/updates/android/UpdatesPlugin.kt +6 -0
  320. package/vendor/@akanjs/native/plugins/updates/ios/UpdatesPlugin.swift +6 -0
  321. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +106 -18
  322. package/vendor/use-agentic/AgentSession.ts +7 -2
  323. package/vendor/use-agentic/ToolRunner.ts +44 -5
  324. package/vendor/use-agentic/types.ts +2 -0
  325. package/vendor/use-agentic/useAgentTool.ts +4 -2
  326. package/webkit/McpAppAgent.ts +2 -1
  327. package/webkit/bootCsr.tsx +1 -1
  328. package/webkit/useCsrValues.ts +5 -1
  329. package/webkit/usePageTool.tsx +1 -0
  330. package/store/databaseStateNames.ts +0 -24
  331. package/store/formSetterNames.ts +0 -18
  332. package/types/store/databaseStateNames.d.ts +0 -18
  333. package/types/store/formSetterNames.d.ts +0 -11
@@ -12,6 +12,7 @@ import { Children, cloneElement, isValidElement, type ReactElement, type ReactNo
12
12
  import { getExplicitPageConfigKeys, resolvePageState } from "../client/frameConfig";
13
13
  import { RouteGuards } from "../client/route/RouteGuards";
14
14
  import { resolveHeadResult } from "./head";
15
+ import { RouteShellGate } from "./routeShellGate";
15
16
  import {
16
17
  type AkanRouteSegmentState,
17
18
  createAkanRouteSegments,
@@ -73,12 +74,14 @@ export class RouteElementComposer {
73
74
  searchParams,
74
75
  navKey,
75
76
  notFound,
77
+ gate,
76
78
  }: {
77
79
  pathRoute: PathRoute;
78
80
  params: Record<string, string>;
79
81
  searchParams: Record<string, string | string[]>;
80
82
  navKey?: string;
81
83
  notFound?: RouteNotFoundInPlace;
84
+ gate?: RouteShellGate;
82
85
  }): ReactNode {
83
86
  return RouteElementComposer.composeRenders({
84
87
  renders: RouteElementComposer.#getRenderStack(pathRoute),
@@ -87,6 +90,7 @@ export class RouteElementComposer {
87
90
  searchParams,
88
91
  navKey,
89
92
  notFound,
93
+ gate,
90
94
  });
91
95
  }
92
96
 
@@ -205,6 +209,7 @@ export class RouteElementComposer {
205
209
  pathname,
206
210
  error,
207
211
  digest,
212
+ gate,
208
213
  }: {
209
214
  kind: "not-found" | "error";
210
215
  route: PathRoute | LayoutFallbackRoute;
@@ -213,6 +218,7 @@ export class RouteElementComposer {
213
218
  pathname: string;
214
219
  error?: unknown;
215
220
  digest?: string;
221
+ gate?: RouteShellGate;
216
222
  }): Promise<ReactNode | null> {
217
223
  const layoutStack = [...route.renderRootLayouts, ...route.renderLayouts];
218
224
  for (let index = layoutStack.length - 1; index >= 0; index--) {
@@ -225,7 +231,7 @@ export class RouteElementComposer {
225
231
  ...layoutStack.slice(0, index + 1),
226
232
  RouteElementComposer.#makeFallbackRouteRender({ kind, fallback, pathname, error, digest }),
227
233
  ];
228
- return RouteElementComposer.composeRenders({ renders, params, searchParams });
234
+ return RouteElementComposer.composeRenders({ renders, params, searchParams, gate });
229
235
  }
230
236
  return null;
231
237
  }
@@ -238,6 +244,7 @@ export class RouteElementComposer {
238
244
  searchParams,
239
245
  navKey,
240
246
  notFound,
247
+ gate,
241
248
  }: {
242
249
  renders: RouteRender[];
243
250
  /** Layouts above `renders` that this element leaves out (a patch) but whose `.notFound()` still applies. */
@@ -247,7 +254,10 @@ export class RouteElementComposer {
247
254
  searchParams: Record<string, string | string[]>;
248
255
  navKey?: string;
249
256
  notFound?: RouteNotFoundInPlace;
257
+ /** Holds the response until every level above the first `.loading()` has rendered. */
258
+ gate?: RouteShellGate;
250
259
  }): ReactNode {
260
+ const levelGates = gate ? RouteElementComposer.#gateLevels(renders, gate) : [];
251
261
  let element: ReactNode = null;
252
262
  for (let i = renders.length - 1; i >= 0; i--) {
253
263
  const routeRender = renders[i];
@@ -255,16 +265,23 @@ export class RouteElementComposer {
255
265
  const loadingFallback = RouteElementComposer.#composeLoadingFallback(renders.slice(i), params);
256
266
  const suspenseKey =
257
267
  navKey && loadingFallback != null && i === renders.length - 1 ? `akan-loading:${navKey}` : undefined;
268
+ const levelGate = levelGates[i];
269
+ const level = (
270
+ <RouteElementComposer.AsyncRender
271
+ routeRender={routeRender}
272
+ params={params}
273
+ searchParams={searchParams}
274
+ notFound={notFound ? { ...notFound, owners: [...owners, ...renders.slice(0, i)] } : undefined}
275
+ gate={levelGate}
276
+ >
277
+ {element}
278
+ </RouteElementComposer.AsyncRender>
279
+ );
280
+
258
281
  element = (
259
282
  <Suspense key={suspenseKey} fallback={loadingFallback}>
260
- <RouteElementComposer.AsyncRender
261
- routeRender={routeRender}
262
- params={params}
263
- searchParams={searchParams}
264
- notFound={notFound ? { ...notFound, owners: [...owners, ...renders.slice(0, i)] } : undefined}
265
- >
266
- {element}
267
- </RouteElementComposer.AsyncRender>
283
+ {level}
284
+ {routeRender.Loading && levelGate ? <RouteShellGate.Wait until={levelGate.opened} /> : null}
268
285
  </Suspense>
269
286
  );
270
287
  const segment = segments?.[i];
@@ -286,13 +303,16 @@ export class RouteElementComposer {
286
303
  params,
287
304
  searchParams,
288
305
  notFound,
306
+ gate,
289
307
  }: {
290
308
  routeRender: RouteRender;
291
309
  children: ReactNode;
292
310
  params: Record<string, string>;
293
311
  searchParams: Record<string, string | string[]>;
294
312
  notFound?: RouteNotFoundInPlace & { owners: RouteRender[] };
313
+ gate?: RouteShellGate;
295
314
  }) => {
315
+ gate?.enter();
296
316
  try {
297
317
  return RouteElementComposer.#normalizeReactNode(
298
318
  await routeRender.render({ children, params, searchParams } as never),
@@ -301,6 +321,8 @@ export class RouteElementComposer {
301
321
  if (!notFound || (error as { digest?: unknown } | null)?.digest !== "AKAN_NOT_FOUND") throw error;
302
322
  notFound.onNotFound();
303
323
  return await RouteElementComposer.#renderNotFoundInPlace(notFound, params, searchParams);
324
+ } finally {
325
+ gate?.leave();
304
326
  }
305
327
  };
306
328
 
@@ -380,6 +402,17 @@ export class RouteElementComposer {
380
402
  return [...layoutConfigs, ...(pageConfig ? [pageConfig] : [])];
381
403
  }
382
404
 
405
+ static #gateLevels(renders: RouteRender[], gate: RouteShellGate): RouteShellGate[] {
406
+ let current = gate;
407
+ const gates = renders.map((routeRender) => {
408
+ if (routeRender?.Loading) current = new RouteShellGate();
409
+ return current;
410
+ });
411
+ for (const levelGate of new Set([gate, ...gates]))
412
+ levelGate.hold(gates.filter((held, index) => held === levelGate && renders[index]).length);
413
+ return gates;
414
+ }
415
+
383
416
  static #composeLoadingFallback(renders: RouteRender[], params: Record<string, string>): ReactNode {
384
417
  let element: ReactNode = null;
385
418
  for (let i = renders.length - 1; i >= 0; i--) {
@@ -0,0 +1,42 @@
1
+ export class RouteShellGate {
2
+
3
+ static Wait = async ({ until }: { until: Promise<null> }) => await until;
4
+
5
+ readonly opened: Promise<null>;
6
+ #open: () => void = () => undefined;
7
+ #holding = false;
8
+ #rendering = 0;
9
+ #activity = 0;
10
+
11
+ constructor() {
12
+ this.opened = new Promise<null>((resolve) => {
13
+ this.#open = () => resolve(null);
14
+ });
15
+ }
16
+
17
+ get holding(): boolean {
18
+ return this.#holding;
19
+ }
20
+
21
+ hold(levels: number): void {
22
+ this.#holding = levels > 0;
23
+ if (levels === 0) this.#open();
24
+ }
25
+
26
+ enter(): void {
27
+ this.#rendering += 1;
28
+ this.#activity += 1;
29
+ }
30
+
31
+ leave(): void {
32
+ this.#rendering -= 1;
33
+ const activity = ++this.#activity;
34
+ setImmediate(() =>
35
+ setImmediate(() => {
36
+ if (this.#rendering > 0 || this.#activity !== activity) return;
37
+ this.#holding = false;
38
+ this.#open();
39
+ }),
40
+ );
41
+ }
42
+ }
@@ -15,6 +15,7 @@ import { SignalResolver } from "../resolver";
15
15
  import type { HttpRoutes, SignalRouteOptions, WebsocketRoutes } from "../types";
16
16
  import { AppWsData } from "./appWsData";
17
17
  import type { HostAllowlist } from "./hostAllowlist";
18
+ import { ServerLimits } from "./serverLimits";
18
19
  import { WsUpgrade } from "./wsUpgrade";
19
20
 
20
21
  export interface HmrStateSource {
@@ -43,6 +44,10 @@ export interface ApiRouteInputs {
43
44
  upgradeAppWs: (req: Request, data: AppWsData) => boolean;
44
45
  webProxyRunner?: WebProxyRunner | null;
45
46
  hostAllowlist?: HostAllowlist | null;
47
+ bodyLimits?: ServerLimits | null;
48
+ /** Off behind the gateway, which already counted the chunked body it streams on. */
49
+ countChunkedBodies?: boolean;
50
+ idleTimeout?: number;
46
51
  }
47
52
 
48
53
  type RouteValue = NonNullHttpRoutes[keyof NonNullHttpRoutes];
@@ -72,6 +77,9 @@ export class ApiRouter {
72
77
  upgradeAppWs,
73
78
  webProxyRunner,
74
79
  hostAllowlist,
80
+ bodyLimits,
81
+ countChunkedBodies = true,
82
+ idleTimeout = 0,
75
83
  }: ApiRouteInputs): NonNullHttpRoutes {
76
84
  const endpointEntries = Object.entries(routes ?? {}).map(
77
85
  ([p, handler]) =>
@@ -109,7 +117,11 @@ export class ApiRouter {
109
117
  served,
110
118
  new Set([...endpointEntries.map(([path]) => path), ...Object.keys(renderEnvRoutes ?? {})]),
111
119
  );
112
- return ApiRouter.#dropForgedForwarding(hostAllowlist ? ApiRouter.#guardHosts(swept, hostAllowlist) : swept);
120
+ const limited = bodyLimits ? ApiRouter.#limitBodies(swept, bodyLimits, countChunkedBodies) : swept;
121
+ const routed = ApiRouter.#dropForgedForwarding(
122
+ hostAllowlist ? ApiRouter.#guardHosts(limited, hostAllowlist) : limited,
123
+ );
124
+ return idleTimeout ? ApiRouter.#holdOpen(routed, idleTimeout) : routed;
113
125
  }
114
126
 
115
127
  static buildWebsocketHandlers({
@@ -257,6 +269,57 @@ export class ApiRouter {
257
269
  ) as NonNullHttpRoutes;
258
270
  }
259
271
 
272
+ static #limitBodies(routes: NonNullHttpRoutes, limits: ServerLimits, countChunked: boolean): NonNullHttpRoutes {
273
+ return Object.fromEntries(
274
+ Object.entries(routes).map(([path, route]) => [
275
+ path,
276
+ route instanceof Response
277
+ ? route
278
+ : ApiRouter.#mapRoute(route, (handler) => (req) => {
279
+ const refused = limits.refuseDeclared(req);
280
+ if (refused) return refused;
281
+ const limit = countChunked && !req.headers.has("content-length") ? limits.bodyLimitOf(req) : null;
282
+ if (limit === null || !req.body) return handler(req);
283
+ let exceeded = false;
284
+ const body = ServerLimits.limitBody(req.body, limit, () => {
285
+ exceeded = true;
286
+ });
287
+ const counted = new Request(req.url, {
288
+ method: req.method,
289
+ headers: req.headers,
290
+ body,
291
+ signal: req.signal,
292
+ duplex: "half",
293
+ } as RequestInit);
294
+ return (async () => {
295
+ try {
296
+ const response = await handler(copyBunRequestFields(counted, req));
297
+ return exceeded ? ServerLimits.payloadTooLarge() : response;
298
+ } catch (error) {
299
+ if (exceeded) return ServerLimits.payloadTooLarge();
300
+ throw error;
301
+ }
302
+ })();
303
+ }),
304
+ ]),
305
+ ) as NonNullHttpRoutes;
306
+ }
307
+
308
+ static #holdOpen(routes: NonNullHttpRoutes, idleTimeout: number): NonNullHttpRoutes {
309
+ return Object.fromEntries(
310
+ Object.entries(routes).map(([path, route]) => [
311
+ path,
312
+ route instanceof Response
313
+ ? route
314
+ : ApiRouter.#mapRoute(
315
+ route,
316
+ (handler) => (req, server?: Bun.Server<unknown>) =>
317
+ server ? ServerLimits.holdOpen(server, req, idleTimeout, () => handler(req)) : handler(req),
318
+ ),
319
+ ]),
320
+ ) as NonNullHttpRoutes;
321
+ }
322
+
260
323
  static #guardHosts(routes: NonNullHttpRoutes, allowlist: HostAllowlist): NonNullHttpRoutes {
261
324
  return Object.fromEntries(
262
325
  Object.entries(routes).map(([path, route]) => [
@@ -0,0 +1,151 @@
1
+ import fs from "node:fs";
2
+ import { Logger } from "akanjs/common";
3
+
4
+ type Env = Record<string, string | undefined>;
5
+
6
+ export class ServerLimits {
7
+
8
+ static readonly maxIdleSeconds = 255;
9
+ static readonly defaultIdleSeconds = 120;
10
+ static readonly defaultUploadBodyBytes = 128 * 1024 * 1024;
11
+ static readonly defaultRequestBodyBytes = 16 * 1024 * 1024;
12
+ static readonly loopbackHost = "127.0.0.1";
13
+ static readonly #agentTurnPath = "/runAgentTurn";
14
+ static readonly #tooLarge = new Error("Request body is over the server's limit");
15
+ static readonly #logger = new Logger("ServerLimits");
16
+ static readonly #warned = new Set<string>();
17
+
18
+ readonly idleTimeout: number;
19
+ readonly wsIdleTimeout: number;
20
+ readonly uploadBodyBytes: number;
21
+ readonly requestBodyBytes: number | null;
22
+
23
+ constructor(env: Env = process.env) {
24
+ const devHosted = env.AKAN_COMMAND_TYPE === "start";
25
+ this.idleTimeout = ServerLimits.#seconds(env, "AKAN_IDLE_TIMEOUT", devHosted ? 0 : ServerLimits.defaultIdleSeconds);
26
+ this.wsIdleTimeout = ServerLimits.#seconds(env, "AKAN_WS_IDLE_TIMEOUT", ServerLimits.defaultIdleSeconds);
27
+ this.uploadBodyBytes = ServerLimits.#bytes(env, "AKAN_MAX_UPLOAD_BODY", ServerLimits.defaultUploadBodyBytes);
28
+ const requestBody = env.AKAN_MAX_REQUEST_BODY?.trim();
29
+ this.requestBodyBytes =
30
+ requestBody === "0" || requestBody?.toLowerCase() === "off"
31
+ ? null
32
+ : Math.min(
33
+ ServerLimits.#bytes(env, "AKAN_MAX_REQUEST_BODY", ServerLimits.defaultRequestBodyBytes),
34
+ this.uploadBodyBytes,
35
+ );
36
+ }
37
+
38
+ static async holdOpen<T extends Response | undefined>(
39
+ server: Pick<Bun.Server<unknown>, "timeout">,
40
+ req: Request,
41
+ idleTimeout: number,
42
+ handle: () => T | Promise<T>,
43
+ ): Promise<T> {
44
+ if (!idleTimeout) return await handle();
45
+ server.timeout(req, 0);
46
+ let upgraded = false;
47
+ try {
48
+ const response = await handle();
49
+ upgraded = response === undefined;
50
+ return response;
51
+ } finally {
52
+ if (!upgraded) server.timeout(req, idleTimeout);
53
+ }
54
+ }
55
+
56
+ static listenHostname(env: Env = process.env, inContainer = ServerLimits.#inContainer(env)): string | undefined {
57
+ const explicit = env.AKAN_LISTEN_HOST?.trim();
58
+ if (explicit) return explicit === "*" ? undefined : explicit;
59
+ return env.AKAN_COMMAND_TYPE === "start" && !inContainer ? ServerLimits.loopbackHost : undefined;
60
+ }
61
+
62
+ static serveOwnerOnly<T>(socketPath: string, serve: () => T): T {
63
+ const previous = process.umask(0o177);
64
+ try {
65
+ return serve();
66
+ } finally {
67
+ process.umask(previous);
68
+ try {
69
+ fs.chmodSync(socketPath, 0o600);
70
+ } catch {
71
+ }
72
+ }
73
+ }
74
+
75
+ static #inContainer(env: Env): boolean {
76
+ if (env.KUBERNETES_SERVICE_HOST) return true;
77
+ return fs.existsSync("/.dockerenv") || fs.existsSync("/run/.containerenv");
78
+ }
79
+
80
+ static #seconds(env: Env, name: string, fallback: number): number {
81
+ const raw = env[name]?.trim();
82
+ if (!raw) return fallback;
83
+ const match = /^(\d+)\s*s?$/i.exec(raw);
84
+ if (match) return Math.min(Number(match[1]), ServerLimits.maxIdleSeconds);
85
+ ServerLimits.#warnUnread(name, raw, "seconds such as 30 or 30s", `${fallback} s`);
86
+ return fallback;
87
+ }
88
+
89
+ static #bytes(env: Env, name: string, fallback: number): number {
90
+ const raw = env[name]?.trim();
91
+ if (!raw) return fallback;
92
+ const match = /^(\d+(?:\.\d+)?)\s*(?:([kmg])(?:i?b)?|b)?$/i.exec(raw);
93
+ const unit = match?.[2]?.toLowerCase();
94
+ const scale = unit === "g" ? 1024 ** 3 : unit === "m" ? 1024 ** 2 : unit === "k" ? 1024 : 1;
95
+ const bytes = match ? Math.floor(Number(match[1]) * scale) : 0;
96
+ if (bytes > 0) return bytes;
97
+ ServerLimits.#warnUnread(name, raw, "a size such as 512mb, 1.5g or 16MiB", `${fallback / 1024 ** 2} MiB`);
98
+ return fallback;
99
+ }
100
+
101
+ static #warnUnread(name: string, raw: string, expected: string, fallback: string) {
102
+ const key = `${name}=${raw}`;
103
+ if (ServerLimits.#warned.has(key)) return;
104
+ ServerLimits.#warned.add(key);
105
+ ServerLimits.#logger.warn(`${name}="${raw}" is not ${expected}; using ${fallback}.`);
106
+ }
107
+
108
+ bodyLimitOf(req: Request): number | null {
109
+ if (this.requestBodyBytes === null || req.method === "GET" || req.method === "HEAD") return null;
110
+ const type = req.headers.get("content-type")?.toLowerCase() ?? "";
111
+ if (type.startsWith("multipart/form-data")) return null;
112
+ return new URL(req.url).pathname.endsWith(ServerLimits.#agentTurnPath) ? null : this.requestBodyBytes;
113
+ }
114
+
115
+ refuseDeclared(req: Request): Response | null {
116
+ const limit = this.bodyLimitOf(req);
117
+ const declared = req.headers.get("content-length");
118
+ if (limit === null || declared === null) return null;
119
+ return Number(declared) > limit ? ServerLimits.payloadTooLarge() : null;
120
+ }
121
+
122
+ static limitBody(
123
+ body: ReadableStream<Uint8Array>,
124
+ limit: number,
125
+ onExceeded?: () => void,
126
+ ): ReadableStream<Uint8Array> {
127
+ let seen = 0;
128
+ return body.pipeThrough(
129
+ new TransformStream<Uint8Array, Uint8Array>({
130
+ transform(chunk, controller) {
131
+ seen += chunk.byteLength;
132
+ if (seen <= limit) return controller.enqueue(chunk);
133
+ onExceeded?.();
134
+ controller.error(ServerLimits.#tooLarge);
135
+ },
136
+ }),
137
+ );
138
+ }
139
+
140
+ static payloadTooLarge(): Response {
141
+ return new Response("Payload Too Large", {
142
+ status: 413,
143
+ headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", connection: "close" },
144
+ });
145
+ }
146
+
147
+ static isPayloadTooLarge(error: unknown): boolean {
148
+ if (error === ServerLimits.#tooLarge) return true;
149
+ return !!error && typeof error === "object" && (error as { cause?: unknown }).cause === ServerLimits.#tooLarge;
150
+ }
151
+ }
@@ -1,4 +1,5 @@
1
1
  import type { AkanChildRole, AkanMetricsReport } from "akanjs/service";
2
+ import { SignalContext } from "akanjs/signal";
2
3
  import { AppInfo } from "../ops/appInfo";
3
4
  import type { OpsRoute } from "../ops/opsRoute";
4
5
  import { StatusAccess } from "../ops/statusAccess";
@@ -14,7 +15,7 @@ export interface SoloAppStatus {
14
15
 
15
16
  export const createSoloAppRoutes = (
16
17
  read: () => SoloAppStatus,
17
- logStream: { handle(req: Request): Response } | null = null,
18
+ logStream: { handle(req: Request, client?: string | null): Response } | null = null,
18
19
  ops: OpsRoute | null = null,
19
20
  access = StatusAccess.fromEnv(),
20
21
  ): HttpRoutes => {
@@ -32,22 +33,32 @@ export const createSoloAppRoutes = (
32
33
  return {
33
34
  "/_akan/app/health": {
34
35
  GET: (req: Request) =>
35
- access.health(req, () => ({ status: read().status, pid: process.pid, solo: true, children: [child()] })),
36
+ access.health(
37
+ req,
38
+ () => ({ status: read().status, pid: process.pid, solo: true, children: [child()] }),
39
+ SignalContext.clientIpOf(req),
40
+ ),
36
41
  },
37
42
  "/_akan/app/metrics": {
38
43
  GET: (req: Request) =>
39
- access.metrics(req, () => ({
40
- rooms: 0,
41
- sockets: 0,
42
- solo: true,
43
- gateway: null,
44
- proxyHop: null,
45
- children: [{ ...child(), metrics: read().metrics }],
46
- })),
44
+ access.metrics(
45
+ req,
46
+ () => ({
47
+ rooms: 0,
48
+ sockets: 0,
49
+ solo: true,
50
+ gateway: null,
51
+ proxyHop: null,
52
+ children: [{ ...child(), metrics: read().metrics }],
53
+ }),
54
+ SignalContext.clientIpOf(req),
55
+ ),
47
56
  },
48
57
  [AppInfo.publicPath]: { GET: () => AppInfo.handlePublic() },
49
58
  "/_akan/bench/ping": { GET: () => new Response("ok") },
50
- ...(logStream ? { "/_akan/app/logs": { GET: (req: Request) => logStream.handle(req) } } : {}),
51
- ...(ops ? { "/_akan/ops/*": (req: Request) => ops.handle(req) } : {}),
59
+ ...(logStream
60
+ ? { "/_akan/app/logs": { GET: (req: Request) => logStream.handle(req, SignalContext.clientIpOf(req)) } }
61
+ : {}),
62
+ ...(ops ? { "/_akan/ops/*": (req: Request) => ops.handle(req, SignalContext.clientIpOf(req)) } : {}),
52
63
  };
53
64
  };
@@ -365,10 +365,11 @@ function forgetPagesOfAnotherAccount(): void {
365
365
 
366
366
  function Root(): ReactNode {
367
367
  const [thenable, setThenable] = useState<RscThenable>(initialThenable);
368
- const [scrollRequest, setScrollRequest] = useState<{ scrollY: number } | null>(null);
368
+ const [scrollRequest, setScrollRequest] = useState<{ scrollY: number; hash?: string } | null>(null);
369
369
 
370
370
  useLayoutEffect(() => {
371
- if (scrollRequest) scrollRestoration.scrollTo(scrollRequest.scrollY);
371
+ if (scrollRequest?.hash) scrollRestoration.scrollToHash(scrollRequest.hash, scrollRequest.scrollY);
372
+ else if (scrollRequest) scrollRestoration.scrollTo(scrollRequest.scrollY);
372
373
  }, [scrollRequest]);
373
374
 
374
375
  globalThis.__AKAN_RSC_CLEAR_CACHE__ = () => {
@@ -458,7 +459,8 @@ function Root(): ReactNode {
458
459
  let ownsEntry = !!options.replace;
459
460
  const replaceOnRedirect = () => ownsEntry;
460
461
  const scrollY = options.scrollY ?? ((options.scrollToTop ?? true) ? 0 : undefined);
461
- const requestScroll = scrollY === undefined ? undefined : () => setScrollRequest({ scrollY });
462
+ const hash = options.scrollY === undefined ? new URL(target).hash : "";
463
+ const requestScroll = scrollY === undefined ? undefined : () => setScrollRequest({ scrollY, hash });
462
464
  try {
463
465
  let nextNode = rscCache.get(target);
464
466
  let notFound = false;
@@ -1,4 +1,5 @@
1
1
  const RESTORE_WINDOW_MS = 2000;
2
+ const FRAGMENT_WAIT_MS = 10_000;
2
3
  const PERSIST_DELAY_MS = 200;
3
4
  const userScrollEvents = ["wheel", "touchstart", "keydown", "pointerdown"] as const;
4
5
 
@@ -68,7 +69,55 @@ export class RscScrollRestoration {
68
69
  const observer = new ResizeObserver(() => {
69
70
  if (Math.abs(window.scrollY - scrollY) > 1) window.scrollTo(0, scrollY);
70
71
  });
71
- const timer = setTimeout(() => cancel(), RESTORE_WINDOW_MS);
72
+ observer.observe(document.body);
73
+ this.#holdWhile(observer, RESTORE_WINDOW_MS);
74
+ }
75
+
76
+ scrollToHash(hash: string, fallbackScrollY = 0) {
77
+ this.#cancelPending?.();
78
+ const id = RscScrollRestoration.#fragmentIdOf(hash);
79
+ const shownTarget = () => {
80
+ const target = id ? document.getElementById(id) : null;
81
+ return target?.closest("[hidden]") ? null : target;
82
+ };
83
+ const target = shownTarget();
84
+ if (target) {
85
+ target.scrollIntoView();
86
+ return;
87
+ }
88
+ window.scrollTo(0, fallbackScrollY);
89
+ if (!id || typeof MutationObserver === "undefined") return;
90
+ const observer = new MutationObserver(() => {
91
+ const appeared = shownTarget();
92
+ if (!appeared) return;
93
+ cancel();
94
+ appeared.scrollIntoView();
95
+ });
96
+ observer.observe(document.documentElement, { childList: true, subtree: true });
97
+ const cancel = this.#holdWhile(observer, FRAGMENT_WAIT_MS);
98
+ }
99
+
100
+ restoreDocumentLoad() {
101
+ if (window.history.scrollRestoration === "manual") {
102
+ this.#manual = true;
103
+ const { scrollY } = RscScrollRestoration.#entryOf(window.history.state);
104
+ if (scrollY !== undefined) {
105
+ this.scrollTo(scrollY);
106
+ return;
107
+ }
108
+ }
109
+
110
+ if (window.location.hash && window.scrollY === 0 && !RscScrollRestoration.#isRevisit())
111
+ this.scrollToHash(window.location.hash);
112
+ }
113
+
114
+ static #isRevisit(): boolean {
115
+ const [entry] = (performance.getEntriesByType?.("navigation") ?? []) as PerformanceNavigationTiming[];
116
+ return entry?.type === "reload" || entry?.type === "back_forward";
117
+ }
118
+
119
+ #holdWhile(observer: { disconnect: () => void }, windowMs: number): () => void {
120
+ const timer = setTimeout(() => cancel(), windowMs);
72
121
  const cancel = () => {
73
122
  observer.disconnect();
74
123
  clearTimeout(timer);
@@ -76,15 +125,8 @@ export class RscScrollRestoration {
76
125
  if (this.#cancelPending === cancel) this.#cancelPending = null;
77
126
  };
78
127
  for (const type of userScrollEvents) window.addEventListener(type, cancel, { capture: true, passive: true });
79
- observer.observe(document.body);
80
128
  this.#cancelPending = cancel;
81
- }
82
-
83
- restoreDocumentLoad() {
84
- if (window.history.scrollRestoration !== "manual") return;
85
- this.#manual = true;
86
- const { scrollY } = RscScrollRestoration.#entryOf(window.history.state);
87
- if (scrollY !== undefined) this.scrollTo(scrollY);
129
+ return cancel;
88
130
  }
89
131
 
90
132
  #schedulePersist() {
@@ -123,6 +165,15 @@ export class RscScrollRestoration {
123
165
  window.history.replaceState({ ...state, __akanRouter: { ...state.__akanRouter, ...fields } }, "");
124
166
  }
125
167
 
168
+ static #fragmentIdOf(hash: string): string {
169
+ const raw = hash.startsWith("#") ? hash.slice(1) : hash;
170
+ try {
171
+ return decodeURIComponent(raw);
172
+ } catch {
173
+ return raw;
174
+ }
175
+ }
176
+
126
177
  static #mintKey() {
127
178
  return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
128
179
  }