akanjs 3.1.0-rc.2 → 3.1.0-rc.20

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 (342) hide show
  1. package/base/baseEnv.ts +5 -2
  2. package/client/device.ts +1 -1
  3. package/client/frameConfig.ts +2 -2
  4. package/client/href.ts +35 -0
  5. package/client/index.ts +1 -0
  6. package/client/makePageProto.tsx +3 -2
  7. package/client/router.ts +6 -4
  8. package/client/rscNavigation.ts +1 -1
  9. package/client/transitionTypes.ts +0 -4
  10. package/client/translator.ts +16 -0
  11. package/client/types.ts +3 -2
  12. package/client/viewportMeta.ts +23 -8
  13. package/client/webManifest.ts +5 -3
  14. package/common/TrustedProxy.ts +14 -0
  15. package/common/codeAgentWire.ts +8 -1
  16. package/common/index.ts +1 -0
  17. package/common/interpolateTranslation.ts +17 -3
  18. package/common/isEmail.ts +1 -1
  19. package/common/localeConfig.ts +10 -4
  20. package/common/markdownSpans.ts +1 -1
  21. package/common/restClient.ts +3 -1
  22. package/common/routeConvention.ts +4 -4
  23. package/common/trimSlashes.ts +11 -0
  24. package/common/types.ts +1 -1
  25. package/constant/fieldInfo.ts +2 -2
  26. package/constant/fieldPlan.ts +80 -0
  27. package/constant/index.ts +3 -0
  28. package/constant/inputParser.ts +148 -0
  29. package/constant/invalidInputError.ts +13 -0
  30. package/constant/serialize.ts +36 -20
  31. package/constant/types.ts +1 -1
  32. package/constant/via.ts +2 -0
  33. package/dictionary/base.dictionary.ts +4 -0
  34. package/dictionary/trans.ts +8 -0
  35. package/document/database.ts +3 -6
  36. package/document/documentSchema.ts +1 -1
  37. package/document/noDocumentError.ts +6 -1
  38. package/fetch/client/fetchClient.ts +2 -1
  39. package/fetch/client/sliceInitHandle.ts +2 -3
  40. package/fetch/fetchType/appliedReturn.type.ts +3 -1
  41. package/fetch/types.ts +4 -2
  42. package/package.json +21 -21
  43. package/server/akanApp.ts +5 -2
  44. package/server/akanServer.ts +34 -14
  45. package/server/artifact/routeClientCache.ts +4 -5
  46. package/server/artifact/routeSeedIndexStore.ts +3 -20
  47. package/server/artifact/routeSeedMatcher.ts +70 -0
  48. package/server/console.ts +1 -1
  49. package/server/di/diLifecycle.ts +16 -7
  50. package/server/imageOptimizer.ts +1 -1
  51. package/server/machine/machineAccount.ts +28 -3
  52. package/server/machine/machineToken.ts +27 -3
  53. package/server/mcp/McpDispatcher.ts +2 -3
  54. package/server/mcp/McpExecutionContext.ts +9 -3
  55. package/server/resolver/CascadeRunner.ts +15 -2
  56. package/server/resolver/signal.resolver.ts +35 -55
  57. package/server/routeState.ts +0 -6
  58. package/server/rscClient.tsx +22 -37
  59. package/server/rscNavigationState.ts +3 -5
  60. package/server/rscScrollRestoration.ts +116 -0
  61. package/server/rscWorker.tsx +12 -19
  62. package/server/sitemap.ts +4 -4
  63. package/server/tunnel/TunnelAgent.ts +9 -1
  64. package/server/webRouter.ts +17 -2
  65. package/service/agent.service.ts +21 -1
  66. package/service/endpointCacheKeys.ts +35 -0
  67. package/service/index.ts +1 -0
  68. package/service/injectInfo.ts +2 -2
  69. package/service/predefinedAdaptor/anthropicLlm.ts +37 -17
  70. package/service/predefinedAdaptor/endpointCacheControl.adaptor.ts +19 -0
  71. package/service/predefinedAdaptor/index.ts +1 -0
  72. package/service/predefinedAdaptor/llm.adaptor.ts +7 -0
  73. package/service/predefinedAdaptor/openaiDialect.ts +53 -17
  74. package/service/predefinedAdaptor/openaiLlm.ts +1 -1
  75. package/service/predefinedAdaptor/sql/SqlDocumentStore.ts +6 -2
  76. package/service/predefinedAdaptor/sql/dialect/postgres.ts +0 -1
  77. package/service/predefinedAdaptor/sql/dialect/sqlite.ts +0 -1
  78. package/service/predefinedAdaptor/storage.adaptor.ts +19 -0
  79. package/signal/endpointCache.ts +23 -7
  80. package/signal/endpointErrorContract.ts +37 -0
  81. package/signal/endpointIdempotency.ts +5 -3
  82. package/signal/endpointInfo.ts +4 -1
  83. package/signal/endpointRateLimit.ts +6 -2
  84. package/signal/exception.ts +4 -1
  85. package/signal/index.ts +1 -0
  86. package/signal/mcp/McpDocument.ts +16 -1
  87. package/signal/mcp/McpUriTemplate.ts +1 -1
  88. package/signal/middlewareChain.ts +105 -0
  89. package/signal/openapi/openapi.ts +46 -2
  90. package/signal/serializer/fetch.serializer.ts +1 -0
  91. package/signal/signalContext.ts +110 -82
  92. package/signal/slice.ts +2 -2
  93. package/signal/trace.ts +8 -2
  94. package/signal/types.ts +13 -0
  95. package/store/action.ts +102 -42
  96. package/store/agent/ScreenImage.ts +166 -0
  97. package/store/agent/ScreenReader.ts +2 -1
  98. package/store/agent/StoreSurfaceSource.ts +41 -2
  99. package/store/agent/index.ts +2 -1
  100. package/store/databaseStateNames.ts +1 -0
  101. package/store/formUploadOf.ts +41 -0
  102. package/store/index.ts +1 -0
  103. package/store/liveInsert.ts +2 -2
  104. package/store/state.ts +9 -1
  105. package/store/stateBuilder.ts +8 -2
  106. package/store/storeInstance.ts +12 -3
  107. package/store/withSelector.ts +2 -2
  108. package/types/client/href.d.ts +11 -0
  109. package/types/client/index.d.ts +1 -0
  110. package/types/client/rscNavigation.d.ts +1 -1
  111. package/types/client/transitionTypes.d.ts +0 -2
  112. package/types/client/translator.d.ts +2 -0
  113. package/types/common/codeAgentWire.d.ts +7 -0
  114. package/types/common/index.d.ts +1 -0
  115. package/types/common/trimSlashes.d.ts +2 -0
  116. package/types/common/types.d.ts +1 -1
  117. package/types/constant/fieldInfo.d.ts +2 -2
  118. package/types/constant/fieldPlan.d.ts +33 -0
  119. package/types/constant/index.d.ts +3 -0
  120. package/types/constant/inputParser.d.ts +11 -0
  121. package/types/constant/invalidInputError.d.ts +8 -0
  122. package/types/constant/types.d.ts +1 -1
  123. package/types/dictionary/base.dictionary.d.ts +1 -1
  124. package/types/dictionary/dictionary.d.ts +3 -3
  125. package/types/dictionary/trans.d.ts +3 -0
  126. package/types/document/database.d.ts +2 -3
  127. package/types/document/documentSchema.d.ts +1 -1
  128. package/types/document/noDocumentError.d.ts +6 -1
  129. package/types/fetch/fetchType/appliedReturn.type.d.ts +3 -1
  130. package/types/fetch/types.d.ts +2 -2
  131. package/types/server/akanServer.d.ts +1 -1
  132. package/types/server/artifact/routeSeedMatcher.d.ts +8 -0
  133. package/types/server/di/diLifecycle.d.ts +1 -0
  134. package/types/server/machine/machineAccount.d.ts +1 -0
  135. package/types/server/machine/machineToken.d.ts +9 -0
  136. package/types/server/routeState.d.ts +1 -4
  137. package/types/server/rscNavigationState.d.ts +2 -3
  138. package/types/server/rscScrollRestoration.d.ts +14 -0
  139. package/types/server/sitemap.d.ts +1 -1
  140. package/types/service/agent.service.d.ts +2 -0
  141. package/types/service/endpointCacheKeys.d.ts +8 -0
  142. package/types/service/index.d.ts +1 -0
  143. package/types/service/injectInfo.d.ts +2 -2
  144. package/types/service/predefinedAdaptor/anthropicLlm.d.ts +9 -4
  145. package/types/service/predefinedAdaptor/endpointCacheControl.adaptor.d.ts +9 -0
  146. package/types/service/predefinedAdaptor/index.d.ts +1 -0
  147. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +7 -0
  148. package/types/service/predefinedAdaptor/openaiDialect.d.ts +20 -5
  149. package/types/service/predefinedAdaptor/storage.adaptor.d.ts +8 -0
  150. package/types/signal/endpointCache.d.ts +2 -0
  151. package/types/signal/endpointErrorContract.d.ts +10 -0
  152. package/types/signal/endpointIdempotency.d.ts +1 -0
  153. package/types/signal/endpointRateLimit.d.ts +1 -1
  154. package/types/signal/exception.d.ts +4 -1
  155. package/types/signal/index.d.ts +1 -0
  156. package/types/signal/middlewareChain.d.ts +11 -0
  157. package/types/signal/signalContext.d.ts +2 -6
  158. package/types/signal/slice.d.ts +1 -1
  159. package/types/signal/trace.d.ts +1 -1
  160. package/types/signal/types.d.ts +12 -0
  161. package/types/store/action.d.ts +2 -0
  162. package/types/store/agent/ScreenImage.d.ts +27 -0
  163. package/types/store/agent/StoreSurfaceSource.d.ts +1 -1
  164. package/types/store/agent/index.d.ts +2 -1
  165. package/types/store/databaseStateNames.d.ts +1 -0
  166. package/types/store/formUploadOf.d.ts +9 -0
  167. package/types/store/index.d.ts +1 -0
  168. package/types/store/state.d.ts +7 -1
  169. package/types/store/stateBuilder.d.ts +2 -1
  170. package/types/store/storeInstance.d.ts +2 -2
  171. package/types/store/withSelector.d.ts +1 -1
  172. package/types/ui/Agent/Composer.d.ts +2 -0
  173. package/types/ui/Agent/Menu.d.ts +2 -0
  174. package/types/ui/CsrImage.d.ts +2 -0
  175. package/types/ui/Data/CardList.d.ts +1 -1
  176. package/types/ui/Data/Dashboard.d.ts +1 -1
  177. package/types/ui/Data/Insight.d.ts +1 -1
  178. package/types/ui/Data/Item.d.ts +2 -2
  179. package/types/ui/Data/ListContainer.d.ts +1 -1
  180. package/types/ui/Data/Pagination.d.ts +1 -1
  181. package/types/ui/Data/TableList.d.ts +1 -1
  182. package/types/ui/Field/Relation.d.ts +13 -10
  183. package/types/ui/Field/index.d.ts +4 -4
  184. package/types/ui/Field/useRelationOptions.d.ts +3 -1
  185. package/types/ui/Field/useRelationWindow.d.ts +14 -0
  186. package/types/ui/Image.d.ts +3 -1
  187. package/types/ui/Load/View.d.ts +1 -1
  188. package/types/ui/Pagination.d.ts +1 -1
  189. package/types/ui/Signal/Arg.d.ts +1 -1
  190. package/types/ui/System/CSR.d.ts +1 -1
  191. package/types/ui/System/Client.d.ts +1 -1
  192. package/types/ui/System/SSR.d.ts +1 -1
  193. package/types/vendor/@akanjs/native/packages/core/src/plugin.d.ts +3 -3
  194. package/types/vendor/@akanjs/native/packages/desktop/src/page-veto.d.ts +1 -1
  195. package/types/vendor/@akanjs/native/packages/desktop/src/plugin.d.ts +1 -1
  196. package/types/vendor/@akanjs/native/plugins/auth-session/src/desktop.d.ts +2 -2
  197. package/types/vendor/@akanjs/native/plugins/auth-session/src/index.d.ts +1 -1
  198. package/types/vendor/@akanjs/native/plugins/autostart/src/desktop.d.ts +2 -2
  199. package/types/vendor/@akanjs/native/plugins/autostart/src/index.d.ts +1 -1
  200. package/types/vendor/@akanjs/native/plugins/badge/src/desktop.d.ts +1 -1
  201. package/types/vendor/@akanjs/native/plugins/badge/src/index.d.ts +1 -1
  202. package/types/vendor/@akanjs/native/plugins/badge/src/web.d.ts +1 -1
  203. package/types/vendor/@akanjs/native/plugins/biometric/src/index.d.ts +1 -1
  204. package/types/vendor/@akanjs/native/plugins/camera/src/desktop.d.ts +1 -1
  205. package/types/vendor/@akanjs/native/plugins/camera/src/index.d.ts +1 -1
  206. package/types/vendor/@akanjs/native/plugins/camera/src/web.d.ts +1 -1
  207. package/types/vendor/@akanjs/native/plugins/clipboard/src/desktop.d.ts +2 -2
  208. package/types/vendor/@akanjs/native/plugins/clipboard/src/index.d.ts +1 -1
  209. package/types/vendor/@akanjs/native/plugins/clipboard/src/web.d.ts +1 -1
  210. package/types/vendor/@akanjs/native/plugins/contacts/src/index.d.ts +1 -1
  211. package/types/vendor/@akanjs/native/plugins/device/src/desktop.d.ts +2 -2
  212. package/types/vendor/@akanjs/native/plugins/device/src/index.d.ts +1 -1
  213. package/types/vendor/@akanjs/native/plugins/device/src/web.d.ts +1 -1
  214. package/types/vendor/@akanjs/native/plugins/dialog/src/desktop.d.ts +1 -1
  215. package/types/vendor/@akanjs/native/plugins/dialog/src/index.d.ts +1 -1
  216. package/types/vendor/@akanjs/native/plugins/dialog/src/web.d.ts +1 -1
  217. package/types/vendor/@akanjs/native/plugins/dock/src/desktop.d.ts +1 -1
  218. package/types/vendor/@akanjs/native/plugins/dock/src/index.d.ts +1 -1
  219. package/types/vendor/@akanjs/native/plugins/file-picker/src/desktop.d.ts +2 -2
  220. package/types/vendor/@akanjs/native/plugins/file-picker/src/web.d.ts +1 -1
  221. package/types/vendor/@akanjs/native/plugins/filesystem/src/desktop.d.ts +2 -2
  222. package/types/vendor/@akanjs/native/plugins/filesystem/src/web.d.ts +1 -1
  223. package/types/vendor/@akanjs/native/plugins/haptics/src/index.d.ts +1 -1
  224. package/types/vendor/@akanjs/native/plugins/haptics/src/web.d.ts +1 -1
  225. package/types/vendor/@akanjs/native/plugins/http/src/desktop.d.ts +2 -2
  226. package/types/vendor/@akanjs/native/plugins/http/src/web.d.ts +1 -1
  227. package/types/vendor/@akanjs/native/plugins/keep-awake/src/desktop.d.ts +2 -2
  228. package/types/vendor/@akanjs/native/plugins/keep-awake/src/index.d.ts +1 -1
  229. package/types/vendor/@akanjs/native/plugins/keep-awake/src/web.d.ts +1 -1
  230. package/types/vendor/@akanjs/native/plugins/opener/src/desktop.d.ts +2 -2
  231. package/types/vendor/@akanjs/native/plugins/opener/src/index.d.ts +1 -1
  232. package/types/vendor/@akanjs/native/plugins/opener/src/web.d.ts +1 -1
  233. package/types/vendor/@akanjs/native/plugins/preferences/src/desktop.d.ts +1 -1
  234. package/types/vendor/@akanjs/native/plugins/preferences/src/index.d.ts +1 -1
  235. package/types/vendor/@akanjs/native/plugins/preferences/src/web.d.ts +1 -1
  236. package/types/vendor/@akanjs/native/plugins/secure-storage/src/desktop.d.ts +2 -2
  237. package/types/vendor/@akanjs/native/plugins/secure-storage/src/index.d.ts +1 -1
  238. package/types/vendor/@akanjs/native/plugins/share/src/index.d.ts +1 -1
  239. package/types/vendor/@akanjs/native/plugins/share/src/web.d.ts +1 -1
  240. package/types/vendor/@akanjs/native/plugins/single-instance/src/desktop.d.ts +2 -2
  241. package/types/vendor/@akanjs/native/plugins/splash-screen/src/index.d.ts +1 -1
  242. package/types/vendor/@akanjs/native/plugins/splash-screen/src/web.d.ts +1 -1
  243. package/types/vendor/@akanjs/native/plugins/sqlite/src/desktop.d.ts +2 -2
  244. package/types/vendor/@akanjs/native/plugins/toast/src/index.d.ts +1 -1
  245. package/types/vendor/@akanjs/native/plugins/toast/src/web.d.ts +1 -1
  246. package/types/vendor/@akanjs/native/plugins/window/src/index.d.ts +2 -2
  247. package/types/vendor/@akanjs/native/plugins/window-state/src/desktop.d.ts +2 -2
  248. package/types/vendor/@akanjs/native/plugins/window-state/src/index.d.ts +1 -1
  249. package/types/vendor/use-agentic/Compaction.d.ts +3 -1
  250. package/types/vendor/use-agentic/Reference.d.ts +9 -1
  251. package/types/vendor/use-agentic/ToolOutput.d.ts +6 -1
  252. package/types/vendor/use-agentic/ToolReturn.d.ts +18 -0
  253. package/types/vendor/use-agentic/Transcript.d.ts +7 -0
  254. package/types/vendor/use-agentic/index.d.ts +1 -0
  255. package/types/vendor/use-agentic/types.d.ts +6 -0
  256. package/types/webkit/useDebounce.d.ts +1 -1
  257. package/ui/Agent/Chat.tsx +1 -0
  258. package/ui/Agent/Composer.tsx +2 -0
  259. package/ui/Agent/Menu.tsx +5 -1
  260. package/ui/Agent/agentSessionOf.ts +15 -8
  261. package/ui/Agent/mentionDraft.ts +2 -3
  262. package/ui/Agent/useReferenceMenu.ts +1 -0
  263. package/ui/Agent/useSlashMenu.ts +1 -0
  264. package/ui/Clipboard.tsx +1 -0
  265. package/ui/CsrImage.tsx +3 -1
  266. package/ui/Data/CardList.tsx +1 -1
  267. package/ui/Data/Dashboard.tsx +1 -1
  268. package/ui/Data/Insight.tsx +1 -1
  269. package/ui/Data/Item.tsx +5 -2
  270. package/ui/Data/ListContainer.tsx +2 -2
  271. package/ui/Data/Pagination.tsx +1 -1
  272. package/ui/Data/TableList.tsx +1 -1
  273. package/ui/DraggableList.tsx +1 -0
  274. package/ui/Field/Collection.tsx +4 -0
  275. package/ui/Field/Relation.tsx +33 -14
  276. package/ui/Field/useRelationOptions.ts +33 -5
  277. package/ui/Field/useRelationWindow.ts +50 -0
  278. package/ui/Image.tsx +4 -4
  279. package/ui/Input.tsx +1 -0
  280. package/ui/Layout/Sider.tsx +1 -1
  281. package/ui/Link/Anchor.tsx +7 -5
  282. package/ui/Load/Units.tsx +17 -7
  283. package/ui/Load/View.tsx +2 -2
  284. package/ui/Model/Edit.tsx +1 -1
  285. package/ui/Model/EditModal.tsx +11 -5
  286. package/ui/Model/New.tsx +1 -1
  287. package/ui/Model/SureToRemove.tsx +1 -0
  288. package/ui/Model/ViewEditModal.tsx +17 -5
  289. package/ui/Model/ViewModal.tsx +2 -2
  290. package/ui/ObjectId.tsx +5 -1
  291. package/ui/Pagination.tsx +1 -1
  292. package/ui/Signal/Arg.tsx +1 -3
  293. package/ui/Signal/Doc.tsx +5 -1
  294. package/ui/System/CSR.tsx +1 -1
  295. package/ui/System/Client.tsx +1 -1
  296. package/ui/System/Reconnect.tsx +1 -1
  297. package/ui/System/SSR.tsx +3 -36
  298. package/ui/ToggleSelect.tsx +2 -0
  299. package/ui/styles.css +10 -103
  300. package/vendor/@akanjs/native/native/ios/Sources/AkanNativeBridge.swift +3 -2
  301. package/vendor/@akanjs/native/packages/cli/src/lib/addons.ts +4 -1
  302. package/vendor/@akanjs/native/packages/cli/src/lib/apk.ts +2 -2
  303. package/vendor/@akanjs/native/packages/cli/src/lib/codegen.ts +1 -1
  304. package/vendor/@akanjs/native/packages/cli/src/lib/csp.ts +4 -3
  305. package/vendor/@akanjs/native/packages/cli/src/lib/googleservices.ts +7 -1
  306. package/vendor/@akanjs/native/packages/cli/src/lib/hmr.ts +8 -5
  307. package/vendor/@akanjs/native/packages/cli/src/lib/html.ts +18 -3
  308. package/vendor/@akanjs/native/packages/cli/src/lib/manifestmerge.ts +1 -1
  309. package/vendor/@akanjs/native/packages/cli/src/lib/plist.ts +2 -4
  310. package/vendor/@akanjs/native/packages/cli/src/lib/toolchains.ts +1 -1
  311. package/vendor/@akanjs/native/packages/cli/src/lib/updates.ts +10 -4
  312. package/vendor/@akanjs/native/packages/cli/src/lib/xml.ts +4 -3
  313. package/vendor/@akanjs/native/packages/cli/src/platforms/android.ts +3 -3
  314. package/vendor/@akanjs/native/packages/core/src/kernel.ts +3 -3
  315. package/vendor/@akanjs/native/packages/core/src/live.ts +1 -0
  316. package/vendor/@akanjs/native/packages/core/src/plugin.ts +5 -3
  317. package/vendor/@akanjs/native/packages/desktop/src/host.ts +13 -5
  318. package/vendor/@akanjs/native/packages/desktop/src/lifecycle.ts +1 -0
  319. package/vendor/@akanjs/native/packages/desktop/src/page-veto.ts +1 -1
  320. package/vendor/@akanjs/native/packages/desktop/src/plugin.ts +4 -1
  321. package/vendor/@akanjs/native/plugins/camera/src/web.ts +3 -3
  322. package/vendor/@akanjs/native/plugins/file-picker/src/index.ts +4 -1
  323. package/vendor/@akanjs/native/plugins/local-notifications/src/web.ts +1 -1
  324. package/vendor/@akanjs/native/plugins/secure-storage/src/desktop.ts +1 -1
  325. package/vendor/@akanjs/native/plugins/share/src/options.ts +1 -1
  326. package/vendor/@akanjs/native/plugins/single-instance/src/desktop.ts +3 -2
  327. package/vendor/@akanjs/native/plugins/updates/src/desktop.ts +4 -1
  328. package/vendor/@akanjs/native/plugins/window/src/index.ts +2 -2
  329. package/vendor/use-agentic/Compaction.ts +38 -7
  330. package/vendor/use-agentic/Reference.ts +16 -9
  331. package/vendor/use-agentic/ToolOutput.ts +25 -3
  332. package/vendor/use-agentic/ToolReturn.ts +32 -0
  333. package/vendor/use-agentic/ToolRunner.ts +15 -13
  334. package/vendor/use-agentic/Transcript.ts +38 -2
  335. package/vendor/use-agentic/WIRE.md +20 -0
  336. package/vendor/use-agentic/index.ts +1 -0
  337. package/vendor/use-agentic/types.ts +6 -0
  338. package/webkit/McpAppAgent.ts +12 -2
  339. package/webkit/RenderLayer.tsx +23 -4
  340. package/webkit/useDebounce.ts +23 -14
  341. package/client/viewTransition.ts +0 -10
  342. package/types/client/viewTransition.d.ts +0 -3
package/base/baseEnv.ts CHANGED
@@ -81,8 +81,11 @@ const globalWithPrefix = globalThis as typeof globalThis & { __AKAN_PREFIX__?: R
81
81
  export const normalizeRoutePrefix = (value: string | undefined | null): string | undefined => {
82
82
  const trimmed = value?.trim();
83
83
  if (!trimmed) return undefined;
84
- const normalized = `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
85
- return normalized === "/" ? undefined : normalized;
84
+ let start = 0;
85
+ let end = trimmed.length;
86
+ while (start < end && trimmed[start] === "/") start += 1;
87
+ while (end > start && trimmed[end - 1] === "/") end -= 1;
88
+ return start === end ? undefined : `/${trimmed.slice(start, end)}`;
86
89
  };
87
90
 
88
91
  /**
package/client/device.ts CHANGED
@@ -11,7 +11,7 @@ type DebugPayload = Record<string, unknown>;
11
11
 
12
12
  const globalWithProcess = globalThis as typeof globalThis & { process?: ProcessEnvLike };
13
13
 
14
- const debugSessionId = Math.random().toString(36).slice(2, 8);
14
+ const debugSessionId = crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
15
15
  let debugSeq = 0;
16
16
 
17
17
  const FRAME_TRACE_LIMIT = 2000;
@@ -1,4 +1,4 @@
1
- import { isRecord } from "akanjs/common";
1
+ import { isRecord, trimSlashes } from "akanjs/common";
2
2
  import type { PageConfig, PageSafeAreaConfig, PageState, ResponsiveInset, SsrRenderMode } from "./csrTypes";
3
3
  import { transitionTypes } from "./transitionTypes";
4
4
 
@@ -246,7 +246,7 @@ const getPlatformFrameProfile = (platform: DevicePlatform): PageConfig => {
246
246
  };
247
247
 
248
248
  const getRouteRoleFrameDefaults = (path: string, platform: DevicePlatform, basePath?: string): PageConfig => {
249
- const normalizedBasePath = basePath?.replace(/^\/+|\/+$/g, "");
249
+ const normalizedBasePath = trimSlashes(basePath ?? "");
250
250
  const depth = path
251
251
  .split("/")
252
252
  .filter(Boolean)
package/client/href.ts ADDED
@@ -0,0 +1,35 @@
1
+ export class Href {
2
+ static readonly #blocked = new Set(["javascript:", "vbscript:", "data:"]);
3
+
4
+ static #parse(href: string, base?: string): URL | null {
5
+ try {
6
+ return new URL(href, base);
7
+ } catch {
8
+ return null;
9
+ }
10
+ }
11
+
12
+ /** External is what the browser takes off-site: any absolute URL with a scheme, and `//`/`\\` network-path
13
+ * forms that name a host. javascript:, vbscript: and data: never count. */
14
+ static isExternal(href: string): boolean {
15
+
16
+ const absolute = Href.#parse(href);
17
+ if (absolute) return !Href.#blocked.has(absolute.protocol);
18
+
19
+ const value = href.replace(/[\t\n\r]/g, "").replace(/^[\u0000-\u0020]+/, "");
20
+ return /^[\\/]{2}/.test(value) && !!Href.#parse(value, "https://localhost")?.hostname;
21
+ }
22
+
23
+ /** An explicit http(s) scheme in any case; `//host` and `httpbin/x` do not qualify. */
24
+ static hasWebScheme(href: string): boolean {
25
+ return /^https?:/i.test(href);
26
+ }
27
+
28
+ /** Canonical absolute URL for an external href: follows the browser on an http(s) document base; on a native
29
+ * `app://` base an absolute URL resolves as written and a network path resolves against https:. */
30
+ static toExternalUrl(href: string, base: string): string {
31
+ const baseUrl = Href.#parse(base);
32
+ if (baseUrl && /^https?:$/.test(baseUrl.protocol)) return Href.#parse(href, baseUrl.href)?.href ?? href;
33
+ return (Href.#parse(href) ?? Href.#parse(href, "https://localhost"))?.href ?? href;
34
+ }
35
+ }
package/client/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./csrTypes";
7
7
  export * from "./decorators";
8
8
  export * from "./device";
9
9
  export * from "./frameConfig";
10
+ export * from "./href";
10
11
  export * from "./locale";
11
12
  export * from "./makePageProto";
12
13
  export * from "./mcpView";
@@ -28,11 +28,12 @@ const getPageInfo = (): { locale: string; path: string } => {
28
28
  const activePath = Translator.getActivePath();
29
29
  if (activePath) return { locale: activeLocale ?? defaultLocale, path: activePath };
30
30
  if (getEnv().side !== "server") {
31
- const [, firstSegment = "", ...rest] = csrLocation.pathname.split("/");
31
+ const pathname = Translator.getRenderPath() ?? csrLocation.pathname;
32
+ const [, firstSegment = "", ...rest] = pathname.split("/");
32
33
  const hasLocalePrefix = localeSet.has(firstSegment);
33
34
 
34
35
  const locale = activeLocale ?? (hasLocalePrefix ? firstSegment : defaultLocale);
35
- return { locale, path: activePath ?? (hasLocalePrefix ? `/${rest.join("/")}` : csrLocation.pathname) };
36
+ return { locale, path: hasLocalePrefix ? `/${rest.join("/")}` : pathname };
36
37
  }
37
38
  const h = untrackedHeaders();
38
39
 
package/client/router.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getEnv } from "akanjs/base";
2
- import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths } from "akanjs/common";
2
+ import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths, trimSlashes } from "akanjs/common";
3
3
  import { csrLocation } from "./csrLocation";
4
+ import { Href } from "./href";
4
5
  import { isMcpView } from "./mcpView";
5
6
 
6
7
  export interface RouteOptions {
@@ -99,7 +100,7 @@ const normalizeRoutePath = (path: string | undefined) => {
99
100
  if (!normalized) return undefined;
100
101
  const [pathWithoutHash, hash = ""] = normalized.split("#");
101
102
  const [pathname, search = ""] = pathWithoutHash.split("?");
102
- const routePath = `/${pathname.replace(/^\/+|\/+$/g, "")}`;
103
+ const routePath = `/${trimSlashes(pathname)}`;
103
104
  return `${routePath === "/" ? "/" : routePath}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
104
105
  };
105
106
 
@@ -158,7 +159,8 @@ export const getPathInfo = (requestUrl: string, lang: string, prefix: string) =>
158
159
  ? pathWithSubRoute.slice(prefixLength)
159
160
  : pathWithSubRoute;
160
161
  const subRoute = prefix ? `/${prefix}` : "";
161
- const pathname = path.startsWith("http") ? path : path === "/" ? `/${lang}${subRoute}` : `/${lang}${subRoute}${path}`;
162
+
163
+ const pathname = Href.hasWebScheme(path) ? path : path === "/" ? `/${lang}${subRoute}` : `/${lang}${subRoute}${path}`;
162
164
  const href = `${pathname}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
163
165
  return { path, pathname, hash, search, href };
164
166
  };
@@ -453,7 +455,7 @@ class Router {
453
455
  const h = requestHeaders();
454
456
  const req = getRequest();
455
457
  const reqPathname = req ? new URL(req.url).pathname : "";
456
- const langFromPath = reqPathname.split("/").filter(Boolean)[0];
458
+ const langFromPath = reqPathname.split("/").find(Boolean);
457
459
  const lang = (h.get("x-locale") ?? langFromPath ?? this.#lang) as string;
458
460
  const basePath = getServerBasePath(reqPathname, lang, h.get("x-base-path") ?? undefined, this.#prefix);
459
461
  const { pathname, href: fullHref } = getPathInfo(href, lang, shouldExposeBasePath() ? basePath : "");
@@ -5,7 +5,7 @@ declare global {
5
5
  var __AKAN_RSC_NAVIGATE__:
6
6
  | ((
7
7
  href: string,
8
- options?: { replace?: boolean; scrollToTop?: boolean; shallow?: boolean; pop?: boolean },
8
+ options?: { replace?: boolean; scrollToTop?: boolean; scrollY?: number; shallow?: boolean },
9
9
  ) => Promise<void>)
10
10
  | undefined;
11
11
  var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
@@ -1,6 +1,2 @@
1
1
  export const transitionTypes = ["none", "fade", "bottomUp", "stack", "scaleOut"] as const;
2
2
  export type TransitionType = (typeof transitionTypes)[number];
3
-
4
- /** The class `ui/styles.css` animates a page by when a navigation plays `transition` forward (push) or back (pop). */
5
- export const viewTransitionClassOf = (transition: Exclude<TransitionType, "none">, direction: "push" | "pop") =>
6
- `akan-${transition}-${direction}` as const;
@@ -18,6 +18,8 @@ interface TranslatorState {
18
18
  activeLocale?: string;
19
19
 
20
20
  activePath?: string;
21
+
22
+ renderPath?: string;
21
23
  }
22
24
 
23
25
  const TRANSLATOR_STATE_KEY = "__AKAN_TRANSLATOR_STATE__";
@@ -57,6 +59,20 @@ export class Translator {
57
59
  static getActivePath(): string | undefined {
58
60
  return getTranslatorState().activePath;
59
61
  }
62
+ static renderAt<T>(path: string | undefined, render: () => T): T {
63
+ if (!path) return render();
64
+ const state = getTranslatorState();
65
+ const outer = state.renderPath;
66
+ state.renderPath = path;
67
+ try {
68
+ return render();
69
+ } finally {
70
+ state.renderPath = outer;
71
+ }
72
+ }
73
+ static getRenderPath(): string | undefined {
74
+ return getTranslatorState().renderPath;
75
+ }
60
76
  static markHydrated() {
61
77
  delete getTranslatorState().activePath;
62
78
  }
package/client/types.ts CHANGED
@@ -102,8 +102,9 @@ const slugFontPart = (value: string) =>
102
102
  value
103
103
  .trim()
104
104
  .toLowerCase()
105
- .replace(/[^a-z0-9]+/g, "-")
106
- .replace(/^-+|-+$/g, "") || "font";
105
+ .split(/[^a-z0-9]+/)
106
+ .filter(Boolean)
107
+ .join("-") || "font";
107
108
 
108
109
  export interface ModelsProps<M extends { id: string }> {
109
110
  className?: string;
@@ -7,6 +7,9 @@ export interface ThemeColorTag {
7
7
 
8
8
  /** Turns a root layout's `.viewport()` into the `viewport` and `theme-color` meta tags every document shell writes. */
9
9
  export class ViewportMeta {
10
+ static readonly #offMedia = "not all";
11
+ static readonly #ownAttr = "data-akan-theme-color";
12
+
10
13
  static content(viewport: RouteViewport | undefined, { native = false }: { native?: boolean } = {}): string {
11
14
  const parts = ["width=device-width", "initial-scale=1"];
12
15
  if (viewport?.fitCover ?? native) parts.push("viewport-fit=cover");
@@ -39,18 +42,30 @@ export class ViewportMeta {
39
42
  const tags = ViewportMeta.themeColors(viewport, theme);
40
43
  if (!tags.length) return;
41
44
  const existing = [...document.head.querySelectorAll<HTMLMetaElement>('meta[name="theme-color"]')];
42
-
43
- for (const meta of existing.slice(tags.length)) meta.setAttribute("media", "not all");
44
- tags.forEach((tag, idx) => {
45
- const meta = existing[idx] ?? ViewportMeta.#ensureMeta("theme-color", true);
45
+ const live = existing.filter((meta) => meta.getAttribute("media") !== ViewportMeta.#offMedia);
46
+ const isCurrent =
47
+ live.length === tags.length &&
48
+ tags.every(
49
+ (tag, idx) => live[idx]?.content === tag.content && live[idx]?.getAttribute("media") === (tag.media ?? null),
50
+ );
51
+ if (isCurrent) return;
52
+
53
+ for (const meta of existing) {
54
+ if (meta.hasAttribute(ViewportMeta.#ownAttr)) meta.remove();
55
+ else meta.setAttribute("media", ViewportMeta.#offMedia);
56
+ }
57
+ for (const tag of tags) {
58
+ const meta = document.createElement("meta");
59
+ meta.name = "theme-color";
46
60
  meta.content = tag.content;
47
61
  if (tag.media) meta.setAttribute("media", tag.media);
48
- else meta.removeAttribute("media");
49
- });
62
+ meta.setAttribute(ViewportMeta.#ownAttr, "");
63
+ document.head.appendChild(meta);
64
+ }
50
65
  }
51
66
 
52
- static #ensureMeta(name: string, fresh = false): HTMLMetaElement {
53
- const found = fresh ? null : document.head.querySelector<HTMLMetaElement>(`meta[name="${name}"]`);
67
+ static #ensureMeta(name: string): HTMLMetaElement {
68
+ const found = document.head.querySelector<HTMLMetaElement>(`meta[name="${name}"]`);
54
69
  if (found) return found;
55
70
  const meta = document.createElement("meta");
56
71
  meta.name = name;
@@ -1,8 +1,10 @@
1
+ import { trimSlashes } from "akanjs/common";
2
+
1
3
  /** Where an SSR document links its root layout's `.manifest()`; the server answers it from that layout. */
2
4
  export const webManifestHref = (basePath?: string | null) =>
3
5
  basePath ? `/${basePath}/manifest.webmanifest` : "/manifest.webmanifest";
4
6
 
5
- const trimSlashes = (value: string) => value.trim().replace(/^\/+|\/+$/g, "");
7
+ const trimBasePath = (value: string) => trimSlashes(value.trim());
6
8
 
7
9
  /** The basePath a `/manifest.webmanifest` request asks for: by a visible prefix, else by the request's basePath. */
8
10
  export const getWebManifestBasePath = (
@@ -12,9 +14,9 @@ export const getWebManifestBasePath = (
12
14
  ): string | null | undefined => {
13
15
  const segments = pathname.trim().split("/").filter(Boolean);
14
16
  if (segments.at(-1) !== "manifest.webmanifest" || segments.length > 2) return undefined;
15
- const known = basePaths.map(trimSlashes);
17
+ const known = basePaths.map(trimBasePath);
16
18
  if (segments.length === 2) return known.includes(segments[0] ?? "") ? (segments[0] ?? null) : undefined;
17
- const fromHeader = trimSlashes(headerBasePath ?? "");
19
+ const fromHeader = trimBasePath(headerBasePath ?? "");
18
20
  return known.includes(fromHeader) ? fromHeader : null;
19
21
  };
20
22
 
@@ -3,8 +3,12 @@ import { clientAddressFromHeaders, normalizeIpAddress } from "./clientAddress";
3
3
  export class TrustedProxy {
4
4
  static #cidrs: { bytes: Uint8Array; bits: number }[] | null = null;
5
5
  static #trustAll: boolean | null = null;
6
+
7
+ static readonly #verdicts = new Map<string, boolean>();
8
+ static readonly #verdictLimit = 1024;
6
9
 
7
10
  static configure(list: string | null | undefined) {
11
+ TrustedProxy.#verdicts.clear();
8
12
  TrustedProxy.#trustAll = list?.trim() === "*";
9
13
  TrustedProxy.#cidrs = (list ?? "")
10
14
  .split(",")
@@ -19,6 +23,7 @@ export class TrustedProxy {
19
23
  static reset() {
20
24
  TrustedProxy.#cidrs = null;
21
25
  TrustedProxy.#trustAll = null;
26
+ TrustedProxy.#verdicts.clear();
22
27
  }
23
28
 
24
29
  static #load() {
@@ -29,6 +34,15 @@ export class TrustedProxy {
29
34
  if (!address) return false;
30
35
  TrustedProxy.#load();
31
36
  if (TrustedProxy.#trustAll) return true;
37
+ const known = TrustedProxy.#verdicts.get(address);
38
+ if (known !== undefined) return known;
39
+ const verdict = TrustedProxy.#judge(address);
40
+ if (TrustedProxy.#verdicts.size >= TrustedProxy.#verdictLimit) TrustedProxy.#verdicts.clear();
41
+ TrustedProxy.#verdicts.set(address, verdict);
42
+ return verdict;
43
+ }
44
+
45
+ static #judge(address: string): boolean {
32
46
  const bytes = TrustedProxy.#toBytes(normalizeIpAddress(address));
33
47
  if (!bytes) return false;
34
48
  if (TrustedProxy.#isLocalRange(bytes)) return true;
@@ -18,6 +18,8 @@ export interface CodeAgentSessionInfo {
18
18
  contextTokens: number | undefined;
19
19
  /** Absent when the model does no reasoning at all, which is not reasoning turned `off`. */
20
20
  effort: CodeAgentEffort | undefined;
21
+ /** Percent of the window that starts a compaction; absent leaves the engine's own, later threshold. */
22
+ autoCompactAt: number | undefined;
21
23
  /** Absent until the session has been asked something. */
22
24
  name: string | undefined;
23
25
  /** `await` keeps a question inside its turn; `suspend` ends the turn (`awaiting`) and the answer opens a new one. */
@@ -186,6 +188,8 @@ export type CodeAgentCommand =
186
188
  | { type: "abort" }
187
189
  | { type: "compact"; instructions?: string }
188
190
  | { type: "set_model"; provider: string; modelId: string }
191
+ /** `percent` in steps of 10; absent, or 100, restores the default. */
192
+ | { type: "set_auto_compact"; percent?: number }
189
193
  | { type: "fork"; entryId: string }
190
194
  | { type: "new_session" }
191
195
  /** Current state, plus every frame after `sinceSeq` when one is given. */
@@ -231,8 +235,11 @@ export const codeAgentEventLabel = (event: CodeAgentEventBody): string => {
231
235
  `session ${event.info.sessionId}`,
232
236
  event.info.model?.name ?? "no model",
233
237
  event.info.contextTokens ? `${Math.round(event.info.contextTokens / 1000)}k ctx` : "unknown ctx",
238
+ event.info.autoCompactAt ? `compacts at ${event.info.autoCompactAt}%` : "",
234
239
  event.info.profile,
235
- ].join(" · ");
240
+ ]
241
+ .filter((part) => !!part)
242
+ .join(" · ");
236
243
  case "turn_start":
237
244
  return "turn start";
238
245
  case "turn_end":
package/common/index.ts CHANGED
@@ -199,6 +199,7 @@ export { sleep } from "./sleep";
199
199
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
200
200
  export { TrustedProxy } from "./TrustedProxy";
201
201
  export { toError } from "./toError";
202
+ export { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
202
203
  export {
203
204
  type TunnelAgentIdentity,
204
205
  type TunnelAttachedFrame,
@@ -1,7 +1,21 @@
1
1
  /** A `{name}` placeholder whose value is absent is left as written, never replaced by "undefined". */
2
2
  export const interpolateTranslation = (message: string, data: Record<string, unknown> | undefined) => {
3
3
  if (!data) return message;
4
- return message.replace(/{([^}]+)}/g, (placeholder, name: string) =>
5
- data[name] === undefined ? placeholder : String(data[name]),
6
- );
4
+
5
+ let result = "";
6
+ let copied = 0;
7
+ let open = message.indexOf("{");
8
+ while (open !== -1) {
9
+ const close = message.indexOf("}", open + 1);
10
+ if (close === -1) break;
11
+ if (close === open + 1) {
12
+ open = message.indexOf("{", close);
13
+ continue;
14
+ }
15
+ const value = data[message.slice(open + 1, close)];
16
+ result += message.slice(copied, open) + (value === undefined ? message.slice(open, close + 1) : String(value));
17
+ copied = close + 1;
18
+ open = message.indexOf("{", copied);
19
+ }
20
+ return result + message.slice(copied);
7
21
  };
package/common/isEmail.ts CHANGED
@@ -1 +1 @@
1
- export const isEmail = (email?: string | null) => !!email && /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,8})+$/.test(email);
1
+ export const isEmail = (email?: string | null) => !!email && /^\w+(?:[.-]\w+)*@\w+(?:[.-]\w+)*\.\w{2,8}$/.test(email);
@@ -28,14 +28,20 @@ export function resolveAkanI18nConfig(input: AkanI18nConfigInput = {}): AkanI18n
28
28
  return { defaultLocale, locales };
29
29
  }
30
30
 
31
+ let lastParsed: { defaultLocale?: string; locales?: string; config: AkanI18nConfig } | null = null;
32
+
31
33
  export function parseAkanI18nEnv(
32
34
  env: Record<string, string | undefined> = {
33
35
  AKAN_PUBLIC_DEFAULT_LOCALE: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_DEFAULT_LOCALE : undefined,
34
36
  AKAN_PUBLIC_LOCALES: typeof process !== "undefined" ? process.env.AKAN_PUBLIC_LOCALES : undefined,
35
37
  },
36
38
  ): AkanI18nConfig {
37
- return resolveAkanI18nConfig({
38
- defaultLocale: env.AKAN_PUBLIC_DEFAULT_LOCALE,
39
- locales: env.AKAN_PUBLIC_LOCALES?.split(","),
40
- });
39
+ const defaultLocale = env.AKAN_PUBLIC_DEFAULT_LOCALE;
40
+ const locales = env.AKAN_PUBLIC_LOCALES;
41
+ if (lastParsed && lastParsed.defaultLocale === defaultLocale && lastParsed.locales === locales)
42
+ return lastParsed.config;
43
+ const resolved = resolveAkanI18nConfig({ defaultLocale, locales: locales?.split(",") });
44
+ const config = Object.freeze({ ...resolved, locales: Object.freeze(resolved.locales) as unknown as string[] });
45
+ lastParsed = { defaultLocale, locales, config };
46
+ return config;
41
47
  }
@@ -9,7 +9,7 @@ export type MarkdownSpan =
9
9
  | { kind: "plain"; text: string };
10
10
 
11
11
  const inline =
12
- /(!?)\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
12
+ /(!?)\[([^[\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)\)|`([^`]+)`|\*\*([\s\S]+?)\*\*|\*([^*\n]+?)\*|~~([\s\S]+?)~~/g;
13
13
 
14
14
  export class MarkdownSpans {
15
15
  static of(text: string): MarkdownSpan[] {
@@ -1,3 +1,5 @@
1
+ import { trimTrailingSlashes } from "./trimSlashes";
2
+
1
3
  export interface RestClientOptions {
2
4
  baseUrl?: string;
3
5
  headers?: HeadersInit;
@@ -16,7 +18,7 @@ export class RestClient {
16
18
 
17
19
  constructor(options: string | RestClientOptions = {}) {
18
20
  const normalized = typeof options === "string" ? { baseUrl: options } : options;
19
- this.baseUrl = normalized.baseUrl?.replace(/\/+$/, "") ?? "";
21
+ this.baseUrl = trimTrailingSlashes(normalized.baseUrl ?? "");
20
22
  this.headers = new Headers(normalized.headers);
21
23
  this.timeout = normalized.timeout;
22
24
  }
@@ -1,3 +1,5 @@
1
+ import { trimSlashes, trimTrailingSlashes } from "./trimSlashes";
2
+
1
3
  const ROUTE_SOURCE_RE = /^\.\/(.+)\.(tsx|ts|jsx|js)$/;
2
4
  const SOURCE_EXT_RE = /\.(tsx|ts|jsx|js)$/;
3
5
  const RESERVED_ROUTE_FILES = new Set(["_layout", "_index", "_overrides"]);
@@ -97,9 +99,7 @@ export function validateSubRoutePageKey(
97
99
  basePaths: Iterable<string>,
98
100
  options: ValidateSubRoutePageKeyOptions = {},
99
101
  ): void {
100
- const allowedBasePaths = [
101
- ...new Set([...basePaths].map((basePath) => basePath.trim().replace(/^\/+|\/+$/g, "")).filter(Boolean)),
102
- ];
102
+ const allowedBasePaths = [...new Set([...basePaths].map((basePath) => trimSlashes(basePath.trim())).filter(Boolean))];
103
103
  if (allowedBasePaths.length === 0) return;
104
104
 
105
105
  const match = ROUTE_SOURCE_RE.exec(key);
@@ -165,7 +165,7 @@ export function tryParseRouteModuleKey(key: string): ParsedRouteModuleKey | null
165
165
 
166
166
  export function normalizeRoutePattern(segments: string[]): string {
167
167
  const parts = segments.map(routeSegmentToPatternPart).filter((part) => part.length > 0);
168
- return `/${parts.join("/")}`.replace(/\/+$/, "") || "/";
168
+ return trimTrailingSlashes(`/${parts.join("/")}`) || "/";
169
169
  }
170
170
 
171
171
  export function routeSegmentToPatternPart(segment: string): string {
@@ -0,0 +1,11 @@
1
+ export const trimTrailingSlashes = (value: string) => {
2
+ let end = value.length;
3
+ while (end > 0 && value[end - 1] === "/") end -= 1;
4
+ return value.slice(0, end);
5
+ };
6
+
7
+ export const trimSlashes = (value: string) => {
8
+ let start = 0;
9
+ while (start < value.length && value[start] === "/") start += 1;
10
+ return trimTrailingSlashes(value.slice(start));
11
+ };
package/common/types.ts CHANGED
@@ -6,7 +6,7 @@ export interface RetryPolicy extends BackoffPolicy {
6
6
  attempts?: number;
7
7
  }
8
8
 
9
- export interface FetchPolicy<Returns = unknown> {
9
+ export interface FetchPolicy<_Returns = unknown> {
10
10
  cache?: boolean | number;
11
11
  crystalize?: boolean;
12
12
  origin?: string;
@@ -276,7 +276,7 @@ export type FieldInfoObjectToFieldObject<Obj extends FieldInfoObject> = {
276
276
 
277
277
  export class ConstantField<
278
278
  FieldType extends ConstantFieldKind = ConstantFieldKind,
279
- Value extends ConstantFieldTypeInput | null = any,
279
+ _Value extends ConstantFieldTypeInput | null = any,
280
280
  FieldValue = any,
281
281
  MapValue = any,
282
282
  Nullable extends boolean = boolean,
@@ -472,7 +472,7 @@ type FieldOption<
472
472
  >
473
473
  >[];
474
474
 
475
- export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...infer Rest]
475
+ export type PlainTypeToFieldType<PlainType> = PlainType extends [infer First, ...unknown[]]
476
476
  ? PlainTypeToFieldType<First>[]
477
477
  : PlainType extends number
478
478
  ? typeof Int | typeof Float
@@ -0,0 +1,80 @@
1
+ import { FIELD_META, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
2
+
3
+ import type { ConstantField, FieldObject } from "./fieldInfo";
4
+
5
+ /**
6
+ * What a response does with a field: leaves it out (`hidden` / `secret`), computes it (`resolve`), copies it (a
7
+ * primitive or a Map), walks into it (an embedded scalar), or loads what it points at (a relation).
8
+ */
9
+ export type FieldPlanKind = "masked" | "computed" | "value" | "scalar" | "relation";
10
+
11
+ export interface FieldPlanEntry {
12
+ key: string;
13
+ field: ConstantField;
14
+ kind: FieldPlanKind;
15
+ /** Points at another stored model, so an input carries its id; independent of `kind`, as `serialize` reads it. */
16
+ isRelationRef: boolean;
17
+ /** The scalar class of a primitive field, which `serialize` calls straight; `null` for everything else. */
18
+ primitive: typeof PrimitiveScalar | null;
19
+ serializeOption: { nullable: boolean; key: string };
20
+ }
21
+ interface PlanOwner {
22
+ [FIELD_META]: FieldObject;
23
+ }
24
+
25
+ export class FieldPlan {
26
+ static #byClass = new WeakMap<object, FieldPlan>();
27
+ static #generation = 0;
28
+ static #computingAsync = new Set<FieldPlan>();
29
+ static of(cls: PlanOwner): FieldPlan {
30
+ const cached = FieldPlan.#byClass.get(cls);
31
+ if (cached) return cached;
32
+ const plan = new FieldPlan(cls[FIELD_META]);
33
+ FieldPlan.#byClass.set(cls, plan);
34
+ return plan;
35
+ }
36
+ static reset(cls: object) {
37
+ FieldPlan.#byClass.delete(cls);
38
+ FieldPlan.#generation += 1;
39
+ }
40
+
41
+ readonly entries: readonly FieldPlanEntry[];
42
+ #resolvesAsync: { value: boolean; generation: number } | null = null;
43
+ constructor(fieldObj: FieldObject) {
44
+ this.entries = Object.entries(fieldObj).map(([key, field]) => ({
45
+ key,
46
+ field,
47
+ kind: FieldPlan.#kindOf(field),
48
+ isRelationRef: field.isClass && !field.isScalar,
49
+ primitive: PrimitiveRegistry.has(field.modelRef) ? (field.modelRef as unknown as typeof PrimitiveScalar) : null,
50
+ serializeOption: { nullable: field.nullable, key },
51
+ }));
52
+ }
53
+
54
+ static #kindOf(field: ConstantField): FieldPlanKind {
55
+ if (field.fieldType === "hidden" || field.fieldType === "secret") return "masked";
56
+ if (field.fieldType === "resolve") return "computed";
57
+ if (!field.isClass) return "value";
58
+ return field.isScalar ? "scalar" : "relation";
59
+ }
60
+
61
+ /** Whether resolving a value of this class awaits anything: a relation, a resolve field, or one in a scalar below. */
62
+ get resolvesAsync(): boolean {
63
+ if (this.#resolvesAsync?.generation === FieldPlan.#generation) return this.#resolvesAsync.value;
64
+
65
+ if (FieldPlan.#computingAsync.has(this)) return true;
66
+ FieldPlan.#computingAsync.add(this);
67
+ try {
68
+ const value = this.entries.some(
69
+ ({ kind, field }) =>
70
+ kind === "computed" ||
71
+ kind === "relation" ||
72
+ (kind === "scalar" && FieldPlan.of(field.modelRef as unknown as PlanOwner).resolvesAsync),
73
+ );
74
+ this.#resolvesAsync = { value, generation: FieldPlan.#generation };
75
+ return value;
76
+ } finally {
77
+ FieldPlan.#computingAsync.delete(this);
78
+ }
79
+ }
80
+ }
package/constant/index.ts CHANGED
@@ -3,10 +3,13 @@ export * from "./constantRegistry";
3
3
  export * from "./crystalize";
4
4
  export * from "./dateSlot";
5
5
  export * from "./fieldInfo";
6
+ export * from "./fieldPlan";
6
7
  export * from "./fieldQueryMeta";
7
8
  export * from "./getDefault";
8
9
  export * from "./hydrationPlan";
9
10
  export * from "./immerify";
11
+ export * from "./inputParser";
12
+ export * from "./invalidInputError";
10
13
  export * from "./labelOf";
11
14
  export * from "./mask";
12
15
  export * from "./purify";