akanjs 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (388) hide show
  1. package/README.ko.md +1 -1
  2. package/README.md +1 -1
  3. package/base/symbols.ts +4 -0
  4. package/client/clientRuntime.ts +8 -0
  5. package/client/cn.ts +7 -0
  6. package/client/csrTypes.ts +2 -0
  7. package/client/frameDebug.ts +3 -2
  8. package/common/Logger.ts +4 -0
  9. package/common/deepObjectify.ts +21 -7
  10. package/common/index.ts +14 -0
  11. package/common/mcpExposure.ts +99 -0
  12. package/common/pathSet.ts +17 -5
  13. package/common/routeConvention.ts +30 -0
  14. package/constant/crystalize.ts +3 -1
  15. package/constant/fieldInfo.ts +6 -0
  16. package/constant/getDefault.ts +37 -9
  17. package/constant/immerify.ts +1 -1
  18. package/constant/index.ts +2 -0
  19. package/constant/labelOf.ts +17 -0
  20. package/constant/mask.ts +60 -0
  21. package/constant/purify.ts +17 -11
  22. package/constant/types.ts +14 -12
  23. package/dictionary/DictionaryLookup.ts +30 -0
  24. package/dictionary/agent.dictionary.ts +31 -0
  25. package/dictionary/agentTurn.dictionary.ts +29 -0
  26. package/dictionary/base.dictionary.ts +8 -0
  27. package/dictionary/dictInfo.ts +8 -0
  28. package/dictionary/dictionary.ts +9 -3
  29. package/dictionary/index.ts +1 -0
  30. package/document/database.ts +21 -0
  31. package/document/filterMeta.ts +7 -0
  32. package/document/index.ts +1 -0
  33. package/document/into.ts +7 -1
  34. package/document/noDocumentError.ts +12 -0
  35. package/fetch/agentTurn.ts +19 -0
  36. package/fetch/client/fetchClient.ts +71 -13
  37. package/fetch/client/wsClient.ts +17 -2
  38. package/fetch/fetchType/endpointFetch.type.ts +10 -4
  39. package/fetch/index.ts +1 -0
  40. package/local/apps/serverLifecycle/serverLifecycle-local.db +0 -0
  41. package/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
  42. package/local/apps/serverLifecycle/serverLifecycle-local.db-wal +0 -0
  43. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db +0 -0
  44. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
  45. package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-wal +0 -0
  46. package/package.json +11 -9
  47. package/server/akanApp.ts +30 -34
  48. package/server/akanOption.ts +65 -7
  49. package/server/akanServer.ts +172 -7
  50. package/server/animatedImage.ts +83 -0
  51. package/server/artifact/routeClientCache.ts +14 -0
  52. package/server/assetEncoding.ts +47 -0
  53. package/server/devtools/signalSerializer.ts +2 -2
  54. package/server/devtools/types.ts +2 -2
  55. package/server/di/diLifecycle.ts +31 -3
  56. package/server/di/predefinedAdaptor.ts +6 -0
  57. package/server/imageOptimizer.ts +33 -46
  58. package/server/index.ts +1 -2
  59. package/server/mcp/McpAuth.ts +177 -0
  60. package/server/mcp/McpDispatcher.ts +245 -0
  61. package/server/mcp/McpEventStream.ts +76 -0
  62. package/server/mcp/McpExecutionContext.ts +105 -0
  63. package/server/mcp/McpRouter.ts +591 -0
  64. package/server/mcp/index.ts +5 -0
  65. package/server/resolver/CascadeRunner.ts +1 -1
  66. package/server/resolver/database.resolver.ts +42 -5
  67. package/server/resolver/service.resolver.ts +29 -0
  68. package/server/resolver/signal.resolver.ts +16 -1
  69. package/server/routeTreeBuilder.ts +2 -44
  70. package/server/rscWorker.tsx +6 -2
  71. package/server/subRouteIndexDocument.tsx +61 -0
  72. package/server/systemPageDocument.tsx +2 -2
  73. package/server/systemPages.tsx +49 -16
  74. package/server/vendor/akanjs-fetch.ts +1 -0
  75. package/server/webRouter.ts +159 -35
  76. package/service/agent.service.ts +14 -0
  77. package/service/base.service.ts +2 -1
  78. package/service/index.ts +1 -0
  79. package/service/predefinedAdaptor/database.adaptor.ts +78 -24
  80. package/service/predefinedAdaptor/deepseekLlm.ts +230 -0
  81. package/service/predefinedAdaptor/index.ts +3 -0
  82. package/service/predefinedAdaptor/insightQuery.ts +183 -0
  83. package/service/predefinedAdaptor/llm.adaptor.ts +76 -0
  84. package/service/predefinedAdaptor/role.adaptor.ts +2 -0
  85. package/service/predefinedAdaptor/schedule.adaptor.ts +12 -5
  86. package/service/serviceModule.ts +17 -0
  87. package/service/types.ts +4 -0
  88. package/signal/agent/AgentCatalogue.ts +165 -0
  89. package/signal/agent/index.ts +1 -0
  90. package/signal/agent.signal.ts +42 -0
  91. package/signal/agentTurn.ts +8 -0
  92. package/signal/agentTurnStream.ts +48 -0
  93. package/signal/base.signal.ts +2 -1
  94. package/signal/endpointInfo.ts +38 -2
  95. package/signal/guard.ts +13 -1
  96. package/signal/guards.ts +52 -1
  97. package/signal/index.ts +6 -0
  98. package/signal/mcp/McpDocument.ts +286 -0
  99. package/signal/mcp/McpProgress.ts +106 -0
  100. package/signal/mcp/McpUriTemplate.ts +85 -0
  101. package/signal/mcp/Msg.ts +368 -0
  102. package/signal/mcp/index.ts +5 -0
  103. package/signal/mcp/mcpProtocol.ts +120 -0
  104. package/signal/middleware.ts +7 -3
  105. package/signal/openapi/openapi.ts +32 -171
  106. package/signal/schema/JsonSchemaBuilder.ts +211 -0
  107. package/signal/schema/index.ts +1 -0
  108. package/signal/signalContext.ts +85 -9
  109. package/signal/slice.ts +9 -9
  110. package/signal/types.ts +1 -1
  111. package/store/action.ts +11 -22
  112. package/store/actionTag.ts +33 -0
  113. package/store/agent/AgentBridge.ts +323 -0
  114. package/store/agent/AgentContext.ts +74 -0
  115. package/store/agent/AgentPrompts.ts +82 -0
  116. package/store/agent/AgentVisibility.ts +68 -0
  117. package/store/agent/ScreenReader.ts +208 -0
  118. package/store/agent/StoreCatalogue.ts +351 -0
  119. package/store/agent/StoreSurfaceSource.ts +122 -0
  120. package/store/agent/index.ts +9 -0
  121. package/store/agent/storeSurface.ts +28 -0
  122. package/store/agent/types.ts +50 -0
  123. package/store/agentic/StToolBuilder.ts +199 -0
  124. package/store/agentic/attachAgentic.ts +22 -0
  125. package/store/agentic/index.ts +5 -0
  126. package/store/agentic/readableValue.ts +17 -0
  127. package/store/agentic/useStExpose.ts +18 -0
  128. package/store/agentic/useStState.ts +35 -0
  129. package/store/baseSt.ts +7 -0
  130. package/store/databaseStateNames.ts +31 -0
  131. package/store/formSetterNames.ts +21 -0
  132. package/store/index.ts +8 -0
  133. package/store/rootStore.ts +3 -1
  134. package/store/sliceRole.ts +36 -0
  135. package/store/state.ts +2 -12
  136. package/store/store.ts +19 -1
  137. package/store/storeInstance.ts +165 -20
  138. package/store/storeRegistry.ts +16 -1
  139. package/store/types.ts +18 -0
  140. package/store/withSelector.ts +7 -8
  141. package/test/registerDom.ts +4 -0
  142. package/types/base/symbols.d.ts +4 -0
  143. package/types/client/cn.d.ts +5 -0
  144. package/types/client/csrTypes.d.ts +2 -0
  145. package/types/common/Logger.d.ts +2 -0
  146. package/types/common/deepObjectify.d.ts +4 -2
  147. package/types/common/index.d.ts +2 -1
  148. package/types/common/mcpExposure.d.ts +60 -0
  149. package/types/common/routeConvention.d.ts +8 -0
  150. package/types/constant/fieldInfo.d.ts +1 -0
  151. package/types/constant/getDefault.d.ts +5 -0
  152. package/types/constant/index.d.ts +2 -0
  153. package/types/constant/labelOf.d.ts +6 -0
  154. package/types/constant/mask.d.ts +34 -0
  155. package/types/constant/purify.d.ts +2 -2
  156. package/types/constant/types.d.ts +2 -2
  157. package/types/dictionary/DictionaryLookup.d.ts +14 -0
  158. package/types/dictionary/agent.dictionary.d.ts +1 -0
  159. package/types/dictionary/agentTurn.dictionary.d.ts +1 -0
  160. package/types/dictionary/base.dictionary.d.ts +1 -1
  161. package/types/dictionary/dictInfo.d.ts +6 -0
  162. package/types/dictionary/dictionary.d.ts +11 -9
  163. package/types/dictionary/index.d.ts +1 -0
  164. package/types/document/database.d.ts +20 -0
  165. package/types/document/filterMeta.d.ts +1 -0
  166. package/types/document/index.d.ts +1 -0
  167. package/types/document/into.d.ts +6 -1
  168. package/types/document/noDocumentError.d.ts +12 -0
  169. package/types/fetch/agentTurn.d.ts +593 -0
  170. package/types/fetch/client/fetchClient.d.ts +10 -0
  171. package/types/fetch/fetchType/endpointFetch.type.d.ts +5 -3
  172. package/types/fetch/index.d.ts +1 -0
  173. package/types/server/akanOption.d.ts +35 -7
  174. package/types/server/akanServer.d.ts +44 -1
  175. package/types/server/animatedImage.d.ts +7 -0
  176. package/types/server/artifact/routeClientCache.d.ts +6 -0
  177. package/types/server/assetEncoding.d.ts +7 -0
  178. package/types/server/devtools/types.d.ts +2 -2
  179. package/types/server/di/predefinedAdaptor.d.ts +4 -1
  180. package/types/server/index.d.ts +0 -2
  181. package/types/server/mcp/McpAuth.d.ts +62 -0
  182. package/types/server/mcp/McpDispatcher.d.ts +54 -0
  183. package/types/server/mcp/McpEventStream.d.ts +18 -0
  184. package/types/server/mcp/McpExecutionContext.d.ts +44 -0
  185. package/types/server/mcp/McpRouter.d.ts +60 -0
  186. package/types/server/mcp/index.d.ts +5 -0
  187. package/types/server/mcp.d.ts +1 -0
  188. package/types/server/subRouteIndexDocument.d.ts +8 -0
  189. package/types/server/systemPageDocument.d.ts +1 -0
  190. package/types/server/systemPages.d.ts +5 -0
  191. package/types/server/vendor/akanjs-fetch.d.ts +1 -0
  192. package/types/service/agent.service.d.ts +12 -0
  193. package/types/service/base.service.d.ts +4 -0
  194. package/types/service/index.d.ts +1 -0
  195. package/types/service/predefinedAdaptor/database.adaptor.d.ts +24 -2
  196. package/types/service/predefinedAdaptor/deepseekLlm.d.ts +64 -0
  197. package/types/service/predefinedAdaptor/index.d.ts +3 -0
  198. package/types/service/predefinedAdaptor/insightQuery.d.ts +50 -0
  199. package/types/service/predefinedAdaptor/llm.adaptor.d.ts +68 -0
  200. package/types/service/predefinedAdaptor/role.adaptor.d.ts +2 -0
  201. package/types/service/predefinedAdaptor/schedule.adaptor.d.ts +10 -6
  202. package/types/service/types.d.ts +4 -1
  203. package/types/signal/agent/AgentCatalogue.d.ts +100 -0
  204. package/types/signal/agent/index.d.ts +1 -0
  205. package/types/signal/agent.d.ts +1 -0
  206. package/types/signal/agent.signal.d.ts +29 -0
  207. package/types/signal/agentTurn.d.ts +9 -0
  208. package/types/signal/agentTurnStream.d.ts +16 -0
  209. package/types/signal/base.signal.d.ts +1 -1
  210. package/types/signal/endpointInfo.d.ts +21 -3
  211. package/types/signal/guard.d.ts +11 -0
  212. package/types/signal/guards.d.ts +22 -1
  213. package/types/signal/index.d.ts +6 -0
  214. package/types/signal/mcp/McpDocument.d.ts +65 -0
  215. package/types/signal/mcp/McpProgress.d.ts +40 -0
  216. package/types/signal/mcp/McpUriTemplate.d.ts +26 -0
  217. package/types/signal/mcp/Msg.d.ts +143 -0
  218. package/types/signal/mcp/index.d.ts +5 -0
  219. package/types/signal/mcp/mcpProtocol.d.ts +107 -0
  220. package/types/signal/mcp.d.ts +1 -0
  221. package/types/signal/openapi/openapi.d.ts +3 -3
  222. package/types/signal/schema/JsonSchemaBuilder.d.ts +47 -0
  223. package/types/signal/schema/index.d.ts +1 -0
  224. package/types/signal/schema.d.ts +1 -0
  225. package/types/signal/signalContext.d.ts +16 -1
  226. package/types/signal/slice.d.ts +8 -8
  227. package/types/signal/types.d.ts +1 -1
  228. package/types/store/actionTag.d.ts +21 -0
  229. package/types/store/agent/AgentBridge.d.ts +77 -0
  230. package/types/store/agent/AgentContext.d.ts +16 -0
  231. package/types/store/agent/AgentPrompts.d.ts +31 -0
  232. package/types/store/agent/AgentVisibility.d.ts +21 -0
  233. package/types/store/agent/ScreenReader.d.ts +11 -0
  234. package/types/store/agent/StoreCatalogue.d.ts +26 -0
  235. package/types/store/agent/StoreSurfaceSource.d.ts +18 -0
  236. package/types/store/agent/index.d.ts +9 -0
  237. package/types/store/agent/storeSurface.d.ts +14 -0
  238. package/types/store/agent/types.d.ts +49 -0
  239. package/types/store/agent.d.ts +1 -0
  240. package/types/store/agentic/StToolBuilder.d.ts +41 -0
  241. package/types/store/agentic/attachAgentic.d.ts +14 -0
  242. package/types/store/agentic/index.d.ts +5 -0
  243. package/types/store/agentic/readableValue.d.ts +3 -0
  244. package/types/store/agentic/useStExpose.d.ts +8 -0
  245. package/types/store/agentic/useStState.d.ts +12 -0
  246. package/types/store/agentic.d.ts +1 -0
  247. package/types/store/baseSt.d.ts +29 -22
  248. package/types/store/databaseStateNames.d.ts +25 -0
  249. package/types/store/formSetterNames.d.ts +16 -0
  250. package/types/store/index.d.ts +7 -0
  251. package/types/store/rootStore.d.ts +5 -1
  252. package/types/store/sliceRole.d.ts +25 -0
  253. package/types/store/store.d.ts +7 -2
  254. package/types/store/storeInstance.d.ts +33 -1
  255. package/types/store/storeRegistry.d.ts +5 -0
  256. package/types/store/types.d.ts +15 -0
  257. package/types/store/withSelector.d.ts +7 -8
  258. package/types/test/registerDom.d.ts +1 -0
  259. package/types/ui/Agent/Approval.d.ts +7 -0
  260. package/types/ui/Agent/Bubble.d.ts +13 -0
  261. package/types/ui/Agent/Chat.d.ts +24 -0
  262. package/types/ui/Agent/Context.d.ts +9 -0
  263. package/types/ui/Agent/Dock.d.ts +16 -0
  264. package/types/ui/Agent/Guide.d.ts +9 -0
  265. package/types/ui/Agent/Section.d.ts +10 -0
  266. package/types/ui/Agent/StateKey.d.ts +16 -0
  267. package/types/ui/Agent/Tool.d.ts +15 -0
  268. package/types/ui/Agent/Transcript.d.ts +13 -0
  269. package/types/ui/Agent/Zone.d.ts +23 -0
  270. package/types/ui/Agent/fetchRunner.d.ts +12 -0
  271. package/types/ui/Agent/index.d.ts +17 -0
  272. package/types/ui/Agent/index_.d.ts +1 -0
  273. package/types/ui/Agent/sessionHistory.d.ts +12 -0
  274. package/types/ui/Agent.d.ts +1 -0
  275. package/types/ui/Badge.d.ts +1 -1
  276. package/types/ui/Button.d.ts +1 -1
  277. package/types/ui/CsrImage.d.ts +1 -1
  278. package/types/ui/Dropdown.d.ts +2 -0
  279. package/types/ui/Image.d.ts +3 -3
  280. package/types/ui/Layout/BottomInset.d.ts +2 -1
  281. package/types/ui/Layout/index.d.ts +1 -1
  282. package/types/ui/Signal/style.d.ts +4 -1
  283. package/types/ui/UiOverride/context.d.ts +14 -2
  284. package/types/ui/agentAttrs.d.ts +14 -0
  285. package/types/ui/index.d.ts +4 -1
  286. package/types/ui/overlayLayer.d.ts +24 -0
  287. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  288. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  289. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  290. package/types/vendor/use-agentic/AgentProvider.d.ts +18 -0
  291. package/types/vendor/use-agentic/AgentScope.d.ts +9 -0
  292. package/types/vendor/use-agentic/AgentSession.d.ts +50 -0
  293. package/types/vendor/use-agentic/Agentic.d.ts +21 -0
  294. package/types/vendor/use-agentic/AgenticSurface.d.ts +44 -0
  295. package/types/vendor/use-agentic/httpRunner.d.ts +15 -0
  296. package/types/vendor/use-agentic/index.d.ts +13 -0
  297. package/types/vendor/use-agentic/surfaceContext.d.ts +5 -0
  298. package/types/vendor/use-agentic/types.d.ts +133 -0
  299. package/types/vendor/use-agentic/useAgent.d.ts +4 -0
  300. package/types/vendor/use-agentic/useAgentGuide.d.ts +6 -0
  301. package/types/vendor/use-agentic/useAgentResource.d.ts +7 -0
  302. package/types/vendor/use-agentic/useAgentState.d.ts +13 -0
  303. package/types/vendor/use-agentic/useAgentTool.d.ts +16 -0
  304. package/types/vendor/use-agentic.d.ts +1 -0
  305. package/types/webkit/index.d.ts +3 -0
  306. package/types/webkit/lazy.d.ts +12 -0
  307. package/types/webkit/useCsrValues.d.ts +3 -3
  308. package/types/webkit/useEscapeKey.d.ts +5 -0
  309. package/types/webkit/useFrameRuntime.d.ts +6 -1
  310. package/types/webkit/useScreenScope.d.ts +18 -0
  311. package/ui/Agent/Approval.tsx +29 -0
  312. package/ui/Agent/Bubble.tsx +74 -0
  313. package/ui/Agent/Chat.tsx +225 -0
  314. package/ui/Agent/Context.tsx +38 -0
  315. package/ui/Agent/Dock.tsx +72 -0
  316. package/ui/Agent/Guide.tsx +16 -0
  317. package/ui/Agent/Section.tsx +24 -0
  318. package/ui/Agent/StateKey.tsx +44 -0
  319. package/ui/Agent/Tool.tsx +66 -0
  320. package/ui/Agent/Transcript.tsx +33 -0
  321. package/ui/Agent/Zone.tsx +48 -0
  322. package/ui/Agent/fetchRunner.ts +30 -0
  323. package/ui/Agent/index.ts +12 -0
  324. package/ui/Agent/index_.tsx +4 -0
  325. package/ui/Agent/sessionHistory.ts +33 -0
  326. package/ui/Badge.tsx +3 -3
  327. package/ui/BottomSheet.tsx +5 -0
  328. package/ui/Button.tsx +6 -2
  329. package/ui/Constant/Doc.tsx +1 -1
  330. package/ui/CsrImage.tsx +1 -1
  331. package/ui/Data/ListContainer.tsx +1 -1
  332. package/ui/DatePicker.tsx +5 -4
  333. package/ui/Dialog/Modal.tsx +12 -15
  334. package/ui/DraggableList.tsx +3 -1
  335. package/ui/Dropdown.tsx +33 -11
  336. package/ui/Field.tsx +30 -16
  337. package/ui/Image.tsx +3 -3
  338. package/ui/Input.tsx +14 -5
  339. package/ui/Layout/BottomInset.tsx +6 -1
  340. package/ui/Load/Units.tsx +11 -2
  341. package/ui/Load/View.tsx +7 -2
  342. package/ui/Loading/ProgressBar.tsx +8 -1
  343. package/ui/Menu.tsx +7 -8
  344. package/ui/Model/EditModal.tsx +37 -2
  345. package/ui/Model/SureToRemove.tsx +2 -1
  346. package/ui/Model/index_.tsx +42 -15
  347. package/ui/ObjectId.tsx +3 -4
  348. package/ui/Pagination.tsx +3 -4
  349. package/ui/Popconfirm.tsx +8 -7
  350. package/ui/Select.tsx +6 -4
  351. package/ui/Signal/RestApi.tsx +43 -16
  352. package/ui/Signal/WebSocket.tsx +1 -1
  353. package/ui/Signal/style.ts +6 -1
  354. package/ui/Switch.tsx +2 -0
  355. package/ui/System/CSR.tsx +6 -2
  356. package/ui/System/ThemeToggle.tsx +55 -33
  357. package/ui/ToggleSelect.tsx +4 -3
  358. package/ui/Tooltip.tsx +2 -1
  359. package/ui/UiOverride/context.ts +14 -2
  360. package/ui/agentAttrs.ts +19 -0
  361. package/ui/index.ts +10 -1
  362. package/ui/overlayLayer.ts +39 -0
  363. package/ui/recipe/badgeRecipe.ts +22 -3
  364. package/ui/recipe/buttonRecipe.ts +51 -2
  365. package/ui/recipe/factory.ts +2 -2
  366. package/ui/recipe/inputRecipe.ts +18 -4
  367. package/vendor/use-agentic/AgentProvider.tsx +39 -0
  368. package/vendor/use-agentic/AgentScope.tsx +20 -0
  369. package/vendor/use-agentic/AgentSession.ts +292 -0
  370. package/vendor/use-agentic/Agentic.tsx +44 -0
  371. package/vendor/use-agentic/AgenticSurface.ts +294 -0
  372. package/vendor/use-agentic/WIRE.md +57 -0
  373. package/vendor/use-agentic/httpRunner.ts +115 -0
  374. package/vendor/use-agentic/index.ts +14 -0
  375. package/vendor/use-agentic/surfaceContext.ts +9 -0
  376. package/vendor/use-agentic/types.ts +144 -0
  377. package/vendor/use-agentic/useAgent.ts +17 -0
  378. package/vendor/use-agentic/useAgentGuide.ts +16 -0
  379. package/vendor/use-agentic/useAgentResource.ts +30 -0
  380. package/vendor/use-agentic/useAgentState.ts +61 -0
  381. package/vendor/use-agentic/useAgentTool.ts +55 -0
  382. package/webkit/bootCsr.tsx +2 -33
  383. package/webkit/index.ts +3 -0
  384. package/webkit/lazy.tsx +27 -3
  385. package/webkit/useCsrValues.ts +121 -4
  386. package/webkit/useEscapeKey.tsx +42 -0
  387. package/webkit/useFrameRuntime.ts +65 -32
  388. package/webkit/useScreenScope.tsx +49 -0
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { getEnv } from "akanjs/base";
4
5
  import {
5
6
  type AkanI18nConfig,
@@ -20,6 +21,7 @@ import {
20
21
  RouteSeedIndexStore,
21
22
  RoutesManifestStore,
22
23
  } from "./artifact";
24
+ import { resolveEncodedSidecar } from "./assetEncoding";
23
25
  import {
24
26
  getClientFacingOrigin,
25
27
  hasRouteCacheInvalidationScope,
@@ -51,7 +53,7 @@ import { type RscRedirectMethod, type RscRedirectStatus, type RscRenderResult, R
51
53
  import { createDefaultSitemapXml, getSitemapBasePath } from "./sitemap";
52
54
  import { SsrFromRscRenderer } from "./ssrFromRscRenderer";
53
55
  import type { RscTraceMetadata, SsrManifest } from "./ssrTypes";
54
- import { createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
56
+ import { createSubRouteIndexResponse, createSystemPageResponse, getSystemPageHomeHref } from "./systemPages";
55
57
  import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
56
58
 
57
59
  const CLIENT_CLOSED_REQUEST_STATUS = 499;
@@ -60,6 +62,18 @@ export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
60
62
  const ROUTE_CACHE_SWEEP_INTERVAL_MS = 60_000;
61
63
  const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
62
64
  const ANDROID_ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
65
+ const FIREBASE_MESSAGING_SW_PATH = "/firebase-messaging-sw.js";
66
+ const FIREBASE_WEB_SDK_VERSION = "12.13.0";
67
+
68
+ interface FirebaseClientEnvConfig {
69
+ apiKey: string;
70
+ authDomain?: string;
71
+ projectId: string;
72
+ storageBucket?: string;
73
+ messagingSenderId: string;
74
+ appId: string;
75
+ vapidKey?: string;
76
+ }
63
77
 
64
78
  export function createRscRedirectResponse(
65
79
  location: string,
@@ -273,7 +287,13 @@ export class WebRouter {
273
287
  #subRoutes: Record<string, string[]>;
274
288
  #rsc: RscWorker;
275
289
  #hub: HmrWsHub | null = null;
276
- #prodMode = process.env.NODE_ENV === "production";
290
+ /**
291
+ * `akan start` is the dev server whatever the environment claims. Its artifact directory carries no routes
292
+ * manifest — only `akan build` writes one — so the production branch cannot build a route on demand and
293
+ * throws on every request instead. `NODE_ENV` arrives by accident often enough (a workspace `.env` Bun loads
294
+ * on its own, a CI image default) that the command which started this process has to outrank it.
295
+ */
296
+ #prodMode = process.env.NODE_ENV === "production" && process.env.AKAN_COMMAND_TYPE !== "start";
277
297
  #builderRpc: BuilderRpc | null;
278
298
  #routeCache: RouteClientCache;
279
299
  #devHmr: DevHmrController | null = null;
@@ -297,10 +317,13 @@ export class WebRouter {
297
317
  #htmlCacheHits = 0;
298
318
  #htmlCacheMisses = 0;
299
319
  #htmlCacheBypass = 0;
320
+ #runtimeManifest: { revision: number; manifest: MergedManifest } | null = null;
300
321
  renderState: RenderState;
301
322
  #seedIndex: RouteSeedIndex;
302
323
  constructor({ artifact, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs }: WebRouterOptions) {
303
324
  this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
325
+ if (process.env.NODE_ENV === "production" && !this.#prodMode)
326
+ this.#logger.warn("[SSR] NODE_ENV=production ignored under `akan start`; serving in dev mode");
304
327
  this.#artifact = artifact;
305
328
  const { subRoutes, ignoredBasePaths } = resolveSubRouteHosts({
306
329
  subRoutes: artifact.subRoutes,
@@ -340,7 +363,7 @@ export class WebRouter {
340
363
  if (prebuilt) {
341
364
  this.#routeCache.seed(prebuilt);
342
365
  await this.#rsc.reload({
343
- clientManifest: this.#mergeRuntimeManifest(this.#routeCache.merged).clientManifest,
366
+ clientManifest: this.#mergeRuntimeManifest().clientManifest,
344
367
  cssAssets: this.renderState.cssAssets,
345
368
  buildId: this.renderState.buildId,
346
369
  });
@@ -486,6 +509,16 @@ export class WebRouter {
486
509
  WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
487
510
  cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
488
511
  }) ?? new Response("Not Found", { status: 404 }),
512
+ [FIREBASE_MESSAGING_SW_PATH]: async () => {
513
+ this.#requestStats.staticAsset += 1;
514
+ const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
515
+ return new Response(WebRouter.#createFirebaseMessagingServiceWorker(firebaseConfig), {
516
+ headers: {
517
+ "Content-Type": "application/javascript; charset=utf-8",
518
+ "Cache-Control": "no-store",
519
+ },
520
+ });
521
+ },
489
522
  "/*": async (req) => {
490
523
  const url = new URL(req.url);
491
524
  if (WebRouter.#isImageOptimizerPath(url.pathname)) {
@@ -553,6 +586,9 @@ export class WebRouter {
553
586
  );
554
587
  }
555
588
 
589
+ const subRouteIndex = this.#localSubRouteIndexResponse(req, url);
590
+ if (subRouteIndex) return await subRouteIndex;
591
+
556
592
  try {
557
593
  this.#requestStats.fullSsr += 1;
558
594
  const manifest = await this.#ensureRoute(url);
@@ -813,15 +849,27 @@ export class WebRouter {
813
849
  this.#logger.verbose(
814
850
  `[route-cache] ensure pathname=${url.pathname} routeId=${matched?.entry.routeId ?? "(none)"} in ${Date.now() - started}ms`,
815
851
  );
816
- return this.#mergeRuntimeManifest(this.#routeCache.snapshot());
852
+ return this.#mergeRuntimeManifest();
817
853
  }
818
854
 
819
- #mergeRuntimeManifest(manifest: MergedManifest): MergedManifest {
820
- return {
821
- ...manifest,
822
- clientManifest: WebRouter.#mergeClientManifest(this.#artifact.rscRuntimeClientManifest, manifest.clientManifest),
823
- ssrManifest: WebRouter.#mergeSsrManifest(this.#artifact.rscRuntimeSsrManifest, manifest.ssrManifest),
855
+ /**
856
+ * Memoized on the route cache's revision. Both the snapshot and the runtime merge copy the whole client manifest
857
+ * and SSR module map, which is a few hundred KB of structure for a small app — and this ran on every request. In
858
+ * production the revision never moves after `seed`, so the manifest is built once; in dev a rebuild bumps it and
859
+ * the next request pays for one fresh copy. The cached object is still a copy, so an in-flight request keeps
860
+ * consuming a stable manifest across an invalidate exactly as the per-request snapshot made it.
861
+ */
862
+ #mergeRuntimeManifest(): MergedManifest {
863
+ const revision = this.#routeCache.revision;
864
+ if (this.#runtimeManifest?.revision === revision) return this.#runtimeManifest.manifest;
865
+ const snapshot = this.#routeCache.snapshot();
866
+ const manifest: MergedManifest = {
867
+ ...snapshot,
868
+ clientManifest: WebRouter.#mergeClientManifest(this.#artifact.rscRuntimeClientManifest, snapshot.clientManifest),
869
+ ssrManifest: WebRouter.#mergeSsrManifest(this.#artifact.rscRuntimeSsrManifest, snapshot.ssrManifest),
824
870
  };
871
+ this.#runtimeManifest = { revision, manifest };
872
+ return manifest;
825
873
  }
826
874
  #renderSystemNotFoundFallbackResponse(req: Request, url: URL): Promise<Response> {
827
875
  return createSystemPageResponse({
@@ -834,6 +882,29 @@ export class WebRouter {
834
882
  });
835
883
  }
836
884
 
885
+ /**
886
+ * A build with subRoutes must keep every route file under `page/<basePath>`, so the site root owns no page and
887
+ * answers 404 — including the URL `akan start` opens a browser on. Locally that reads as a broken app, so serve a
888
+ * picker of the basePaths this build carries instead. Deployed hosts never reach it: `HostBasePathWebProxy`
889
+ * rewrites the root onto the basePath its host maps to before the router sees it.
890
+ */
891
+ #localSubRouteIndexResponse(req: Request, url: URL): Promise<Response> | null {
892
+ if (process.env.AKAN_PUBLIC_ENV !== "local" || !this.#artifact.basePaths.length) return null;
893
+ if (!WebRouter.#isSiteRootPathname(url.pathname, this.#artifact.i18n)) return null;
894
+ return createSubRouteIndexResponse({
895
+ method: req.method,
896
+ locale: WebRouter.#getLocale(url.pathname, this.#artifact.i18n),
897
+ basePaths: this.#artifact.basePaths,
898
+ subRoutes: this.#subRoutes,
899
+ });
900
+ }
901
+
902
+ static #isSiteRootPathname(pathname: string, i18n: AkanI18nConfig): boolean {
903
+ const segments = pathname.split("/").filter(Boolean);
904
+ if (!segments.length) return true;
905
+ return segments.length === 1 && i18n.locales.includes(segments[0] ?? "");
906
+ }
907
+
837
908
  #renderErrorResponse(req: Request, scope: string, err: unknown): Promise<Response> {
838
909
  if (WebRouter.#isExpectedRequestAbort(err)) return Promise.resolve(WebRouter.#clientClosedResponse());
839
910
  const message = err instanceof Error ? err.message : String(err);
@@ -994,6 +1065,79 @@ export class WebRouter {
994
1065
  return process.env.AKAN_APP_DIR ?? path.dirname(Bun.main);
995
1066
  }
996
1067
 
1068
+ static async #resolveFirebaseClientConfig(): Promise<FirebaseClientEnvConfig | null> {
1069
+ const envPath = path.join(WebRouter.#resolveAppDir(), "env", "env.client.ts");
1070
+ if (!fs.existsSync(envPath)) return null;
1071
+ try {
1072
+ const envUrl = pathToFileURL(envPath);
1073
+ envUrl.searchParams.set("t", String(Date.now()));
1074
+ const envModule = (await import(envUrl.href)) as { env?: { firebase?: unknown } };
1075
+ return WebRouter.#normalizeFirebaseClientConfig(envModule.env?.firebase);
1076
+ } catch {
1077
+ return null;
1078
+ }
1079
+ }
1080
+
1081
+ static #normalizeFirebaseClientConfig(config: unknown): FirebaseClientEnvConfig | null {
1082
+ if (!config || typeof config !== "object") return null;
1083
+ const value = config as Partial<Record<keyof FirebaseClientEnvConfig, unknown>>;
1084
+ if (
1085
+ typeof value.apiKey !== "string" ||
1086
+ typeof value.projectId !== "string" ||
1087
+ typeof value.messagingSenderId !== "string" ||
1088
+ typeof value.appId !== "string"
1089
+ ) {
1090
+ return null;
1091
+ }
1092
+ return {
1093
+ apiKey: value.apiKey,
1094
+ ...(typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {}),
1095
+ projectId: value.projectId,
1096
+ ...(typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {}),
1097
+ messagingSenderId: value.messagingSenderId,
1098
+ appId: value.appId,
1099
+ };
1100
+ }
1101
+
1102
+ static #createFirebaseMessagingServiceWorker(config: FirebaseClientEnvConfig | null): string {
1103
+ const configJson = JSON.stringify(config);
1104
+ return `/* Generated by Akan.js. Do not edit. */
1105
+ const firebaseConfig = ${configJson};
1106
+
1107
+ if (firebaseConfig) {
1108
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1109
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1110
+
1111
+ firebase.initializeApp(firebaseConfig);
1112
+ const messaging = firebase.messaging();
1113
+
1114
+ const notificationUrl = (payload) =>
1115
+ payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1116
+
1117
+ messaging.onBackgroundMessage((payload) => {
1118
+ const title = payload?.notification?.title || "";
1119
+ const options = {
1120
+ body: payload?.notification?.body,
1121
+ icon: payload?.notification?.icon,
1122
+ image: payload?.notification?.image,
1123
+ data: {
1124
+ url: notificationUrl(payload),
1125
+ FCM_MSG: payload,
1126
+ },
1127
+ };
1128
+ self.registration.showNotification(title, options);
1129
+ });
1130
+ }
1131
+
1132
+ self.addEventListener("notificationclick", (event) => {
1133
+ const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1134
+ event.notification?.close();
1135
+ if (!url) return;
1136
+ event.waitUntil(clients.openWindow(url));
1137
+ });
1138
+ `;
1139
+ }
1140
+
997
1141
  static #normalizeArtifact(artifact: BaseBuildArtifact, artifactDir: string): BaseBuildArtifact {
998
1142
  const normalizedArtifactDir = path.resolve(artifactDir);
999
1143
  const pagesBundlePath = WebRouter.#resolveArtifactPath(artifact.pagesBundlePath, normalizedArtifactDir);
@@ -1101,16 +1245,12 @@ export class WebRouter {
1101
1245
  headers.set("Last-Modified", new Date(lastModifiedMs).toUTCString());
1102
1246
  if (WebRouter.#isNotModified(req, etag, lastModifiedMs)) return new Response(null, { status: 304, headers });
1103
1247
 
1104
- const gzipPath = `${filePath}.gz`;
1105
- if (WebRouter.#acceptsGzip(req) && WebRouter.#isCompressible(options.contentType)) {
1106
- const gzipFile = Bun.file(gzipPath);
1107
- if (await gzipFile.exists()) {
1108
- const gzipBytes = await gzipFile.bytes();
1109
- headers.set("Content-Encoding", "gzip");
1110
- headers.set("Content-Length", String(gzipBytes.byteLength));
1111
- headers.set("Vary", "Accept-Encoding");
1112
- return new Response(WebRouter.#toArrayBuffer(gzipBytes), { headers });
1113
- }
1248
+ const sidecar = await resolveEncodedSidecar(req, filePath, options.contentType);
1249
+ if (sidecar) {
1250
+ headers.set("Content-Encoding", sidecar.encoding);
1251
+ headers.set("Content-Length", String(sidecar.bytes.byteLength));
1252
+ headers.set("Vary", "Accept-Encoding");
1253
+ return new Response(sidecar.bytes, { headers });
1114
1254
  }
1115
1255
 
1116
1256
  return new Response(file.stream(), { headers });
@@ -1197,22 +1337,6 @@ export class WebRouter {
1197
1337
  return Number.isFinite(sinceMs) && sinceMs >= lastModifiedMs;
1198
1338
  }
1199
1339
 
1200
- static #acceptsGzip(req: Request): boolean {
1201
- const acceptEncoding = req.headers.get("accept-encoding") ?? "";
1202
- return /\bgzip\b/.test(acceptEncoding);
1203
- }
1204
-
1205
- static #isCompressible(contentType: string): boolean {
1206
- const type = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
1207
- return (
1208
- type.startsWith("text/") ||
1209
- type === "application/javascript" ||
1210
- type === "application/json" ||
1211
- type === "application/manifest+json" ||
1212
- type === "image/svg+xml"
1213
- );
1214
- }
1215
-
1216
1340
  static #safeResolve(baseDir: string, urlPath: string): string | null {
1217
1341
  let decoded: string;
1218
1342
  try {
@@ -0,0 +1,14 @@
1
+ import { Err } from "akanjs/dictionary";
2
+ import type { LlmTurnRequest } from "./predefinedAdaptor/llm.adaptor";
3
+ import { LlmAdaptorRole } from "./predefinedAdaptor/role.adaptor";
4
+ import { serve } from "./serve";
5
+
6
+ export class AgentService extends serve("agent" as const, ({ plug }) => ({
7
+ llm: plug(LlmAdaptorRole),
8
+ })) {
9
+ async runTurn(request: LlmTurnRequest, onDelta?: (delta: string) => void) {
10
+ const answer = await this.llm.chat(request, onDelta);
11
+ if (!answer) throw new Err("agent.error.llmUnavailable");
12
+ return { text: answer.text ?? "", toolCalls: answer.toolCalls ?? [], stop: answer.stop };
13
+ }
14
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Base } from "akanjs/signal";
2
+ import { AgentService } from "./agent.service";
2
3
  import { serve } from "./serve";
3
4
  import { ServiceModel } from "./serviceModule";
4
5
 
@@ -11,4 +12,4 @@ export class BaseService extends serve("base" as const, ({ env, signal, memory }
11
12
  }
12
13
  }
13
14
 
14
- export const srv = { base: new ServiceModel(BaseService) };
15
+ export const srv = { base: new ServiceModel(BaseService), agent: new ServiceModel(AgentService) };
package/service/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./adapt";
2
+ export * from "./agent.service";
2
3
  export * from "./base.service";
3
4
  export * from "./injectInfo";
4
5
  export * from "./injectInfo";
@@ -19,6 +19,7 @@ import {
19
19
  documentQueryHelper,
20
20
  encodeDocumentValue,
21
21
  isDocumentUpdateNode,
22
+ NoDocumentError,
22
23
  resolveDocumentUpdate,
23
24
  type SchemaOf,
24
25
  sanitizeJson,
@@ -98,6 +99,9 @@ export interface DocumentStore {
98
99
  removeManyByQuery(
99
100
  query: DocumentQuery,
100
101
  ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
102
+ removeOneByQuery(
103
+ query: DocumentQuery,
104
+ ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
101
105
  bulkWrite(
102
106
  operations: { updateOne: { filter: DocumentQuery; update: DocumentUpdateInput; upsert?: boolean } }[],
103
107
  ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number; upsertedId: string | null }>;
@@ -116,7 +120,7 @@ export interface DocumentStore {
116
120
  exists(query?: DocumentQuery): Promise<string | null>;
117
121
  count(query?: DocumentQuery): Promise<number>;
118
122
  insight(query?: DocumentQuery): Promise<any>;
119
- hydrate(data: DocumentRecord, originalData?: DocumentRecord): any;
123
+ hydrate(data: DocumentRecord, originalData?: DocumentRecord, options?: { track?: boolean }): any;
120
124
  }
121
125
 
122
126
  export interface SqlResultRows<Row = Record<string, unknown>> {
@@ -957,6 +961,17 @@ class UpdateCompiler {
957
961
  }
958
962
  }
959
963
 
964
+ /**
965
+ * Per-document modification state, attached only when a document is hydrated for writing.
966
+ * Non-enumerable so `{ ...doc }` in `toRow` and `Object.entries` in `sanitizeJson` never see it.
967
+ */
968
+ const MODIFICATION_STATE = Symbol("akan.document.modificationState");
969
+
970
+ interface ModificationState {
971
+ isNew: boolean;
972
+ original: Record<string, unknown>;
973
+ }
974
+
960
975
  export class SqlDocumentStore {
961
976
  readonly schema: DocumentSchema;
962
977
  readonly table: string;
@@ -964,6 +979,7 @@ export class SqlDocumentStore {
964
979
  readonly updateCompiler: UpdateCompiler;
965
980
  #insertStmt: AkanSqlStatement | null = null;
966
981
  #readStmtCache = new Map<string, AkanSqlStatement>();
982
+ #docPrototype: object | null = null;
967
983
 
968
984
  constructor(
969
985
  private readonly owner: DocumentDatabaseOwner,
@@ -1086,6 +1102,11 @@ export class SqlDocumentStore {
1086
1102
  return this.updateManyByQuery(query, { removedAt: dayjs() });
1087
1103
  }
1088
1104
 
1105
+ async removeOneByQuery(query: DocumentQuery) {
1106
+
1107
+ return this.updateOneByQuery(query, { removedAt: dayjs() });
1108
+ }
1109
+
1089
1110
  private compiledUpdate(update: DocumentUpdate) {
1090
1111
  const compiled = this.updateCompiler.compile(update);
1091
1112
  return {
@@ -1125,14 +1146,14 @@ export class SqlDocumentStore {
1125
1146
  const rows = await this.prepareReadStmt(
1126
1147
  `SELECT ${this.projectionSql(projection)} FROM ${quoteIdent(this.table)}${join} WHERE ${where} ${order}${limit}${offset}`,
1127
1148
  ).all<ProjectedSqliteDocumentRow>(...args);
1128
- return rows.map((row) => this.hydrate(this.fromProjectedRow(row, projection)));
1149
+ return rows.map((row) => this.hydrate(this.fromProjectedRow(row, projection), undefined, { track: false }));
1129
1150
  }
1130
1151
 
1131
1152
  const star = joins.length ? `${quoteIdent(this.table)}.*` : "*";
1132
1153
  const rows = await this.prepareReadStmt(
1133
1154
  `SELECT ${star} FROM ${quoteIdent(this.table)}${join} WHERE ${where} ${order}${limit}${offset}`,
1134
1155
  ).all<SqliteDocumentRow>(...args);
1135
- return rows.map((row) => this.hydrate(this.fromRow(row)));
1156
+ return rows.map((row) => this.hydrate(this.fromRow(row), undefined, { track: false }));
1136
1157
  }
1137
1158
 
1138
1159
  async findIds(
@@ -1162,13 +1183,13 @@ export class SqlDocumentStore {
1162
1183
 
1163
1184
  async pickOne(query?: DocumentQuery, options: FindOneOptions = {}) {
1164
1185
  const doc = await this.findOne(query, options);
1165
- if (!doc) throw new Error(`No Document (${this.table}): ${JSON.stringify(query)}`);
1186
+ if (!doc) throw new NoDocumentError(`No Document (${this.table}): ${JSON.stringify(query)}`);
1166
1187
  return doc;
1167
1188
  }
1168
1189
 
1169
1190
  async pickById(id: string) {
1170
1191
  const doc = await this.findOne({ id } as DocumentQuery);
1171
- if (!doc) throw new Error(`No Document (${this.table}): ${id}`);
1192
+ if (!doc) throw new NoDocumentError(`No Document (${this.table}): ${id}`);
1172
1193
  return doc;
1173
1194
  }
1174
1195
 
@@ -1450,7 +1471,7 @@ export class SqlDocumentStore {
1450
1471
 
1451
1472
  private async pickByIdForWrite(id: string) {
1452
1473
  const doc = await this.findOneForWrite({ id } as DocumentQuery);
1453
- if (!doc) throw new Error(`No Document (${this.table}): ${id}`);
1474
+ if (!doc) throw new NoDocumentError(`No Document (${this.table}): ${id}`);
1454
1475
  return doc;
1455
1476
  }
1456
1477
 
@@ -1580,49 +1601,82 @@ export class SqlDocumentStore {
1580
1601
  return result;
1581
1602
  }
1582
1603
 
1583
- hydrate(data: DocumentRecord, originalData: DocumentRecord = data) {
1584
- const store = this;
1585
- const original = JSON.parse(JSON.stringify(sanitizeJson(originalData) ?? {})) as Record<string, unknown>;
1604
+ /**
1605
+ * `track` buys `isModified()` and costs a deep clone of the row, so it is decided per call site rather than
1606
+ * paid everywhere. Only the read paths (`find`, and the projected read behind it) opt out — that clone was 42%
1607
+ * of a list query, and a listed document is never the one a save hook runs on. Everything else keeps it, so an
1608
+ * external caller of `hydrate` sees no change.
1609
+ */
1610
+ hydrate(data: DocumentRecord, originalData: DocumentRecord = data, { track = true }: { track?: boolean } = {}) {
1586
1611
  const isNew = !originalData.id;
1587
1612
  const hydratedData = isNew ? this.prepareDocument(data) : data;
1588
- const doc = Object.assign(Object.create(this.database.doc.prototype), hydratedData);
1589
- Object.defineProperties(doc, {
1613
+ const doc = Object.assign(Object.create(this.#documentPrototype()), hydratedData);
1614
+ if (!track) return doc;
1615
+ Object.defineProperty(doc, MODIFICATION_STATE, {
1616
+ value: {
1617
+ isNew,
1618
+
1619
+ original: JSON.parse(JSON.stringify(sanitizeJson(originalData) ?? {})) as Record<string, unknown>,
1620
+ } satisfies ModificationState,
1621
+ });
1622
+ return doc;
1623
+ }
1624
+
1625
+ /**
1626
+ * One prototype per store instead of six closures per document. It extends the model's own document prototype,
1627
+ * so declared chain methods and `instanceof` are unaffected, and every method here is non-enumerable exactly as
1628
+ * the previous per-document `defineProperties` made them.
1629
+ */
1630
+ #documentPrototype() {
1631
+ if (this.#docPrototype) return this.#docPrototype;
1632
+ const store = this;
1633
+ this.#docPrototype = Object.create(this.database.doc.prototype, {
1590
1634
  set: {
1591
- value(patch: DocumentRecord) {
1635
+ value(this: DocumentRecord, patch: DocumentRecord) {
1592
1636
  Object.assign(this, patch);
1593
1637
  return this;
1594
1638
  },
1595
1639
  },
1596
1640
  save: {
1597
- async value() {
1598
- return this.id ? store.update(this.id, this) : store.create(this);
1641
+ async value(this: DocumentRecord) {
1642
+ return this.id ? store.update(this.id as string, this) : store.create(this);
1599
1643
  },
1600
1644
  },
1601
1645
  refresh: {
1602
- async value() {
1603
- Object.assign(this, await store.pickById(this.id));
1646
+ async value(this: DocumentRecord) {
1647
+ Object.assign(this, await store.pickById(this.id as string));
1604
1648
  return this;
1605
1649
  },
1606
1650
  },
1607
1651
  isModified: {
1608
- value(field?: string) {
1609
- if (isNew) return true;
1610
- if (!field) return JSON.stringify(sanitizeJson(this)) !== JSON.stringify(original);
1611
- return JSON.stringify(sanitizeJson(this[field])) !== JSON.stringify(original[field]);
1652
+ value(this: DocumentRecord & { [MODIFICATION_STATE]?: ModificationState }, field?: string) {
1653
+ const state = this[MODIFICATION_STATE];
1654
+ if (!state) throw new Error(SqlDocumentStore.#untrackedModificationMessage(store.table));
1655
+ if (state.isNew) return true;
1656
+ if (!field) return JSON.stringify(sanitizeJson(this)) !== JSON.stringify(state.original);
1657
+ return JSON.stringify(sanitizeJson(this[field])) !== JSON.stringify(state.original[field]);
1612
1658
  },
1613
1659
  },
1614
1660
  toJSON: {
1615
- value() {
1661
+ value(this: DocumentRecord) {
1616
1662
  return sanitizeJson(this);
1617
1663
  },
1618
1664
  },
1619
1665
  toObject: {
1620
- value() {
1666
+ value(this: DocumentRecord) {
1621
1667
  return sanitizeJson(this);
1622
1668
  },
1623
1669
  },
1624
- });
1625
- return doc;
1670
+ }) as object;
1671
+ return this.#docPrototype;
1672
+ }
1673
+
1674
+ static #untrackedModificationMessage(table: string) {
1675
+ return (
1676
+ `isModified() is unavailable on this ${table} document: it was loaded through a read query, which does not ` +
1677
+ `snapshot the row. Call it inside a save hook, or re-load the document through the write path (\`save()\`, ` +
1678
+ `\`update()\`) before comparing.`
1679
+ );
1626
1680
  }
1627
1681
 
1628
1682
  private async runHooks(