@astrale-os/shell-react 0.1.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 (378) hide show
  1. package/LICENSE +202 -0
  2. package/dist/application/apps/app-host.d.ts +31 -0
  3. package/dist/application/apps/app-host.d.ts.map +1 -0
  4. package/dist/application/apps/app-host.js +59 -0
  5. package/dist/application/apps/app-logo.d.ts +19 -0
  6. package/dist/application/apps/app-logo.d.ts.map +1 -0
  7. package/dist/application/apps/app-logo.js +31 -0
  8. package/dist/application/apps/apps.hook.d.ts +12 -0
  9. package/dist/application/apps/apps.hook.d.ts.map +1 -0
  10. package/dist/application/apps/apps.hook.js +51 -0
  11. package/dist/application/apps/open-app.hook.d.ts +10 -0
  12. package/dist/application/apps/open-app.hook.d.ts.map +1 -0
  13. package/dist/application/apps/open-app.hook.js +7 -0
  14. package/dist/application/index.d.ts +18 -0
  15. package/dist/application/index.d.ts.map +1 -0
  16. package/dist/application/index.js +12 -0
  17. package/dist/application/roles.hook.d.ts +7 -0
  18. package/dist/application/roles.hook.d.ts.map +1 -0
  19. package/dist/application/roles.hook.js +23 -0
  20. package/dist/application/spaces/current-space.hook.d.ts +7 -0
  21. package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
  22. package/dist/application/spaces/current-space.hook.js +27 -0
  23. package/dist/application/spaces/current-space.store.d.ts +32 -0
  24. package/dist/application/spaces/current-space.store.d.ts.map +1 -0
  25. package/dist/application/spaces/current-space.store.js +79 -0
  26. package/dist/application/spaces/space.hook.d.ts +5 -0
  27. package/dist/application/spaces/space.hook.d.ts.map +1 -0
  28. package/dist/application/spaces/space.hook.js +25 -0
  29. package/dist/application/spaces/spaces.hook.d.ts +14 -0
  30. package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
  31. package/dist/application/spaces/spaces.hook.js +82 -0
  32. package/dist/application/user/user.hook.d.ts +10 -0
  33. package/dist/application/user/user.hook.d.ts.map +1 -0
  34. package/dist/application/user/user.hook.js +19 -0
  35. package/dist/application/user/user.store.d.ts +35 -0
  36. package/dist/application/user/user.store.d.ts.map +1 -0
  37. package/dist/application/user/user.store.js +51 -0
  38. package/dist/auth/act-as.context.d.ts +40 -0
  39. package/dist/auth/act-as.context.d.ts.map +1 -0
  40. package/dist/auth/act-as.context.js +84 -0
  41. package/dist/auth/auth.hook.d.ts +3 -0
  42. package/dist/auth/auth.hook.d.ts.map +1 -0
  43. package/dist/auth/auth.hook.js +25 -0
  44. package/dist/auth/can.hook.d.ts +40 -0
  45. package/dist/auth/can.hook.d.ts.map +1 -0
  46. package/dist/auth/can.hook.js +119 -0
  47. package/dist/auth/index.d.ts +6 -0
  48. package/dist/auth/index.d.ts.map +1 -0
  49. package/dist/auth/index.js +5 -0
  50. package/dist/auth/self.hook.d.ts +3 -0
  51. package/dist/auth/self.hook.d.ts.map +1 -0
  52. package/dist/auth/self.hook.js +11 -0
  53. package/dist/auth/session-kernel.d.ts +4 -0
  54. package/dist/auth/session-kernel.d.ts.map +1 -0
  55. package/dist/auth/session-kernel.js +21 -0
  56. package/dist/graph/action.d.ts +22 -0
  57. package/dist/graph/action.d.ts.map +1 -0
  58. package/dist/graph/action.js +85 -0
  59. package/dist/graph/async.d.ts +11 -0
  60. package/dist/graph/async.d.ts.map +1 -0
  61. package/dist/graph/async.js +60 -0
  62. package/dist/graph/call/call.hook.d.ts +21 -0
  63. package/dist/graph/call/call.hook.d.ts.map +1 -0
  64. package/dist/graph/call/call.hook.js +23 -0
  65. package/dist/graph/call/stream.hook.d.ts +28 -0
  66. package/dist/graph/call/stream.hook.d.ts.map +1 -0
  67. package/dist/graph/call/stream.hook.js +137 -0
  68. package/dist/graph/graph.context.d.ts +4 -0
  69. package/dist/graph/graph.context.d.ts.map +1 -0
  70. package/dist/graph/graph.context.js +10 -0
  71. package/dist/graph/guards.error.d.ts +9 -0
  72. package/dist/graph/guards.error.d.ts.map +1 -0
  73. package/dist/graph/guards.error.js +7 -0
  74. package/dist/graph/index.d.ts +20 -0
  75. package/dist/graph/index.d.ts.map +1 -0
  76. package/dist/graph/index.js +9 -0
  77. package/dist/graph/memory/base.d.ts +21 -0
  78. package/dist/graph/memory/base.d.ts.map +1 -0
  79. package/dist/graph/memory/base.js +33 -0
  80. package/dist/graph/memory/read/envelope.d.ts +71 -0
  81. package/dist/graph/memory/read/envelope.d.ts.map +1 -0
  82. package/dist/graph/memory/read/envelope.js +29 -0
  83. package/dist/graph/memory/read/idle.d.ts +18 -0
  84. package/dist/graph/memory/read/idle.d.ts.map +1 -0
  85. package/dist/graph/memory/read/idle.js +16 -0
  86. package/dist/graph/memory/read/read.d.ts +9 -0
  87. package/dist/graph/memory/read/read.d.ts.map +1 -0
  88. package/dist/graph/memory/read/read.js +87 -0
  89. package/dist/graph/memory/scope.d.ts +7 -0
  90. package/dist/graph/memory/scope.d.ts.map +1 -0
  91. package/dist/graph/memory/scope.js +25 -0
  92. package/dist/graph/mutate/move.hook.d.ts +7 -0
  93. package/dist/graph/mutate/move.hook.d.ts.map +1 -0
  94. package/dist/graph/mutate/move.hook.js +49 -0
  95. package/dist/graph/mutate/mutate.hook.d.ts +17 -0
  96. package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
  97. package/dist/graph/mutate/mutate.hook.js +45 -0
  98. package/dist/graph/query/children.hook.d.ts +29 -0
  99. package/dist/graph/query/children.hook.d.ts.map +1 -0
  100. package/dist/graph/query/children.hook.js +64 -0
  101. package/dist/graph/query/graph.hook.d.ts +9 -0
  102. package/dist/graph/query/graph.hook.d.ts.map +1 -0
  103. package/dist/graph/query/graph.hook.js +5 -0
  104. package/dist/graph/query/neighbors.hook.d.ts +7 -0
  105. package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
  106. package/dist/graph/query/neighbors.hook.js +79 -0
  107. package/dist/graph/query/node.hook.d.ts +14 -0
  108. package/dist/graph/query/node.hook.d.ts.map +1 -0
  109. package/dist/graph/query/node.hook.js +43 -0
  110. package/dist/graph/query/query.hook.d.ts +14 -0
  111. package/dist/graph/query/query.hook.d.ts.map +1 -0
  112. package/dist/graph/query/query.hook.js +23 -0
  113. package/dist/index.d.ts +43 -0
  114. package/dist/index.d.ts.map +1 -0
  115. package/dist/index.js +34 -0
  116. package/dist/intents/envelope.d.ts +33 -0
  117. package/dist/intents/envelope.d.ts.map +1 -0
  118. package/dist/intents/envelope.js +19 -0
  119. package/dist/intents/index.d.ts +6 -0
  120. package/dist/intents/index.d.ts.map +1 -0
  121. package/dist/intents/index.js +3 -0
  122. package/dist/intents/listen-intent.hook.d.ts +16 -0
  123. package/dist/intents/listen-intent.hook.d.ts.map +1 -0
  124. package/dist/intents/listen-intent.hook.js +75 -0
  125. package/dist/intents/reply.d.ts +9 -0
  126. package/dist/intents/reply.d.ts.map +1 -0
  127. package/dist/intents/reply.js +14 -0
  128. package/dist/intents/send-intent.hook.d.ts +12 -0
  129. package/dist/intents/send-intent.hook.d.ts.map +1 -0
  130. package/dist/intents/send-intent.hook.js +86 -0
  131. package/dist/navigation/adapters/adapter.d.ts +15 -0
  132. package/dist/navigation/adapters/adapter.d.ts.map +1 -0
  133. package/dist/navigation/adapters/adapter.js +1 -0
  134. package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
  135. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
  136. package/dist/navigation/adapters/memory.adapter.js +15 -0
  137. package/dist/navigation/adapters/url.adapter.d.ts +6 -0
  138. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
  139. package/dist/navigation/adapters/url.adapter.js +59 -0
  140. package/dist/navigation/history.d.ts +63 -0
  141. package/dist/navigation/history.d.ts.map +1 -0
  142. package/dist/navigation/history.js +144 -0
  143. package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
  144. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
  145. package/dist/navigation/hooks/ancestors.hook.js +56 -0
  146. package/dist/navigation/hooks/location.hook.d.ts +4 -0
  147. package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
  148. package/dist/navigation/hooks/location.hook.js +17 -0
  149. package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
  150. package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
  151. package/dist/navigation/hooks/navigate.hook.js +33 -0
  152. package/dist/navigation/hooks/scope.hook.d.ts +12 -0
  153. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
  154. package/dist/navigation/hooks/scope.hook.js +21 -0
  155. package/dist/navigation/index.d.ts +16 -0
  156. package/dist/navigation/index.d.ts.map +1 -0
  157. package/dist/navigation/index.js +10 -0
  158. package/dist/navigation/link.d.ts +11 -0
  159. package/dist/navigation/link.d.ts.map +1 -0
  160. package/dist/navigation/link.js +62 -0
  161. package/dist/navigation/location.d.ts +48 -0
  162. package/dist/navigation/location.d.ts.map +1 -0
  163. package/dist/navigation/location.js +88 -0
  164. package/dist/navigation/scope.context.d.ts +64 -0
  165. package/dist/navigation/scope.context.d.ts.map +1 -0
  166. package/dist/navigation/scope.context.js +178 -0
  167. package/dist/navigation/snapshot.d.ts +22 -0
  168. package/dist/navigation/snapshot.d.ts.map +1 -0
  169. package/dist/navigation/snapshot.js +65 -0
  170. package/dist/schema/bind-read.d.ts +37 -0
  171. package/dist/schema/bind-read.d.ts.map +1 -0
  172. package/dist/schema/bind-read.js +205 -0
  173. package/dist/schema/binding.d.ts +21 -0
  174. package/dist/schema/binding.d.ts.map +1 -0
  175. package/dist/schema/binding.js +55 -0
  176. package/dist/schema/domain.hook.d.ts +10 -0
  177. package/dist/schema/domain.hook.d.ts.map +1 -0
  178. package/dist/schema/domain.hook.js +7 -0
  179. package/dist/schema/function.hook.d.ts +9 -0
  180. package/dist/schema/function.hook.d.ts.map +1 -0
  181. package/dist/schema/function.hook.js +40 -0
  182. package/dist/schema/index.d.ts +10 -0
  183. package/dist/schema/index.d.ts.map +1 -0
  184. package/dist/schema/index.js +8 -0
  185. package/dist/schema/kernel.hook.d.ts +10 -0
  186. package/dist/schema/kernel.hook.d.ts.map +1 -0
  187. package/dist/schema/kernel.hook.js +8 -0
  188. package/dist/schema/method.hook.d.ts +14 -0
  189. package/dist/schema/method.hook.d.ts.map +1 -0
  190. package/dist/schema/method.hook.js +49 -0
  191. package/dist/schema/tag.d.ts +11 -0
  192. package/dist/schema/tag.d.ts.map +1 -0
  193. package/dist/schema/tag.js +11 -0
  194. package/dist/schema/types.d.ts +73 -0
  195. package/dist/schema/types.d.ts.map +1 -0
  196. package/dist/schema/types.js +1 -0
  197. package/dist/schema/write.d.ts +15 -0
  198. package/dist/schema/write.d.ts.map +1 -0
  199. package/dist/schema/write.js +45 -0
  200. package/dist/selection/index.d.ts +4 -0
  201. package/dist/selection/index.d.ts.map +1 -0
  202. package/dist/selection/index.js +4 -0
  203. package/dist/selection/live.d.ts +21 -0
  204. package/dist/selection/live.d.ts.map +1 -0
  205. package/dist/selection/live.js +116 -0
  206. package/dist/selection/scope.context.d.ts +26 -0
  207. package/dist/selection/scope.context.d.ts.map +1 -0
  208. package/dist/selection/scope.context.js +100 -0
  209. package/dist/selection/selection.hook.d.ts +4 -0
  210. package/dist/selection/selection.hook.d.ts.map +1 -0
  211. package/dist/selection/selection.hook.js +14 -0
  212. package/dist/session/boot/boot.d.ts +19 -0
  213. package/dist/session/boot/boot.d.ts.map +1 -0
  214. package/dist/session/boot/boot.js +45 -0
  215. package/dist/session/boot/ownership.d.ts +18 -0
  216. package/dist/session/boot/ownership.d.ts.map +1 -0
  217. package/dist/session/boot/ownership.js +39 -0
  218. package/dist/session/boot/status.d.ts +24 -0
  219. package/dist/session/boot/status.d.ts.map +1 -0
  220. package/dist/session/boot/status.js +11 -0
  221. package/dist/session/connection/connection.hook.d.ts +3 -0
  222. package/dist/session/connection/connection.hook.d.ts.map +1 -0
  223. package/dist/session/connection/connection.hook.js +12 -0
  224. package/dist/session/connection/connection.store.d.ts +24 -0
  225. package/dist/session/connection/connection.store.d.ts.map +1 -0
  226. package/dist/session/connection/connection.store.js +31 -0
  227. package/dist/session/index.d.ts +8 -0
  228. package/dist/session/index.d.ts.map +1 -0
  229. package/dist/session/index.js +6 -0
  230. package/dist/session/kernel-client.hook.d.ts +9 -0
  231. package/dist/session/kernel-client.hook.d.ts.map +1 -0
  232. package/dist/session/kernel-client.hook.js +5 -0
  233. package/dist/session/kernel-instance.hook.d.ts +9 -0
  234. package/dist/session/kernel-instance.hook.d.ts.map +1 -0
  235. package/dist/session/kernel-instance.hook.js +10 -0
  236. package/dist/session/provider.d.ts +31 -0
  237. package/dist/session/provider.d.ts.map +1 -0
  238. package/dist/session/provider.js +165 -0
  239. package/dist/session/session.context.d.ts +18 -0
  240. package/dist/session/session.context.d.ts.map +1 -0
  241. package/dist/session/session.context.js +8 -0
  242. package/dist/session/shell.hook.d.ts +4 -0
  243. package/dist/session/shell.hook.d.ts.map +1 -0
  244. package/dist/session/shell.hook.js +15 -0
  245. package/dist/window/adopt.d.ts +27 -0
  246. package/dist/window/adopt.d.ts.map +1 -0
  247. package/dist/window/adopt.js +27 -0
  248. package/dist/window/hooks/close-guard.hook.d.ts +6 -0
  249. package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
  250. package/dist/window/hooks/close-guard.hook.js +90 -0
  251. package/dist/window/hooks/target-node.hook.d.ts +7 -0
  252. package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
  253. package/dist/window/hooks/target-node.hook.js +77 -0
  254. package/dist/window/hooks/views-for.hook.d.ts +8 -0
  255. package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
  256. package/dist/window/hooks/views-for.hook.js +51 -0
  257. package/dist/window/hooks/window.hook.d.ts +4 -0
  258. package/dist/window/hooks/window.hook.d.ts.map +1 -0
  259. package/dist/window/hooks/window.hook.js +14 -0
  260. package/dist/window/hooks/windows.hook.d.ts +15 -0
  261. package/dist/window/hooks/windows.hook.d.ts.map +1 -0
  262. package/dist/window/hooks/windows.hook.js +25 -0
  263. package/dist/window/host.d.ts +31 -0
  264. package/dist/window/host.d.ts.map +1 -0
  265. package/dist/window/host.js +270 -0
  266. package/dist/window/hub.d.ts +41 -0
  267. package/dist/window/hub.d.ts.map +1 -0
  268. package/dist/window/hub.js +100 -0
  269. package/dist/window/index.d.ts +15 -0
  270. package/dist/window/index.d.ts.map +1 -0
  271. package/dist/window/index.js +10 -0
  272. package/dist/window/lifecycle/mount.d.ts +47 -0
  273. package/dist/window/lifecycle/mount.d.ts.map +1 -0
  274. package/dist/window/lifecycle/mount.js +92 -0
  275. package/dist/window/lifecycle/states.d.ts +33 -0
  276. package/dist/window/lifecycle/states.d.ts.map +1 -0
  277. package/dist/window/lifecycle/states.js +7 -0
  278. package/dist/window/placement.d.ts +33 -0
  279. package/dist/window/placement.d.ts.map +1 -0
  280. package/dist/window/placement.js +57 -0
  281. package/dist/window/stage.d.ts +3 -0
  282. package/dist/window/stage.d.ts.map +1 -0
  283. package/dist/window/stage.js +41 -0
  284. package/package.json +59 -0
  285. package/src/application/apps/app-host.tsx +113 -0
  286. package/src/application/apps/app-logo.tsx +56 -0
  287. package/src/application/apps/apps.hook.ts +81 -0
  288. package/src/application/apps/open-app.hook.ts +18 -0
  289. package/src/application/index.ts +22 -0
  290. package/src/application/roles.hook.ts +41 -0
  291. package/src/application/spaces/current-space.hook.ts +43 -0
  292. package/src/application/spaces/current-space.store.ts +110 -0
  293. package/src/application/spaces/space.hook.ts +41 -0
  294. package/src/application/spaces/spaces.hook.ts +113 -0
  295. package/src/application/user/user.hook.ts +27 -0
  296. package/src/application/user/user.store.ts +85 -0
  297. package/src/auth/act-as.context.tsx +124 -0
  298. package/src/auth/auth.hook.ts +36 -0
  299. package/src/auth/can.hook.ts +149 -0
  300. package/src/auth/index.ts +6 -0
  301. package/src/auth/self.hook.ts +14 -0
  302. package/src/auth/session-kernel.ts +24 -0
  303. package/src/graph/action.ts +127 -0
  304. package/src/graph/async.ts +87 -0
  305. package/src/graph/call/call.hook.ts +42 -0
  306. package/src/graph/call/stream.hook.ts +200 -0
  307. package/src/graph/graph.context.ts +14 -0
  308. package/src/graph/guards.error.ts +58 -0
  309. package/src/graph/index.ts +20 -0
  310. package/src/graph/memory/base.ts +50 -0
  311. package/src/graph/memory/read/envelope.ts +98 -0
  312. package/src/graph/memory/read/idle.ts +24 -0
  313. package/src/graph/memory/read/read.ts +103 -0
  314. package/src/graph/memory/scope.ts +29 -0
  315. package/src/graph/mutate/move.hook.ts +70 -0
  316. package/src/graph/mutate/mutate.hook.ts +69 -0
  317. package/src/graph/query/children.hook.ts +136 -0
  318. package/src/graph/query/graph.hook.ts +13 -0
  319. package/src/graph/query/neighbors.hook.ts +124 -0
  320. package/src/graph/query/node.hook.ts +83 -0
  321. package/src/graph/query/query.hook.ts +42 -0
  322. package/src/index.ts +202 -0
  323. package/src/intents/envelope.ts +52 -0
  324. package/src/intents/index.ts +6 -0
  325. package/src/intents/listen-intent.hook.ts +92 -0
  326. package/src/intents/reply.ts +24 -0
  327. package/src/intents/send-intent.hook.ts +111 -0
  328. package/src/navigation/adapters/adapter.ts +13 -0
  329. package/src/navigation/adapters/memory.adapter.ts +22 -0
  330. package/src/navigation/adapters/url.adapter.ts +61 -0
  331. package/src/navigation/history.ts +170 -0
  332. package/src/navigation/hooks/ancestors.hook.ts +71 -0
  333. package/src/navigation/hooks/location.hook.ts +21 -0
  334. package/src/navigation/hooks/navigate.hook.ts +43 -0
  335. package/src/navigation/hooks/scope.hook.ts +34 -0
  336. package/src/navigation/index.ts +17 -0
  337. package/src/navigation/link.tsx +110 -0
  338. package/src/navigation/location.ts +122 -0
  339. package/src/navigation/scope.context.tsx +267 -0
  340. package/src/navigation/snapshot.ts +79 -0
  341. package/src/schema/bind-read.ts +319 -0
  342. package/src/schema/binding.ts +91 -0
  343. package/src/schema/domain.hook.ts +16 -0
  344. package/src/schema/function.hook.ts +68 -0
  345. package/src/schema/index.ts +25 -0
  346. package/src/schema/kernel.hook.ts +17 -0
  347. package/src/schema/method.hook.ts +103 -0
  348. package/src/schema/tag.ts +19 -0
  349. package/src/schema/types.ts +150 -0
  350. package/src/schema/write.ts +80 -0
  351. package/src/selection/index.ts +5 -0
  352. package/src/selection/live.ts +142 -0
  353. package/src/selection/scope.context.tsx +151 -0
  354. package/src/selection/selection.hook.ts +18 -0
  355. package/src/session/boot/boot.ts +57 -0
  356. package/src/session/boot/ownership.ts +54 -0
  357. package/src/session/boot/status.ts +32 -0
  358. package/src/session/connection/connection.hook.ts +16 -0
  359. package/src/session/connection/connection.store.ts +58 -0
  360. package/src/session/index.ts +8 -0
  361. package/src/session/kernel-client.hook.ts +13 -0
  362. package/src/session/kernel-instance.hook.ts +22 -0
  363. package/src/session/provider.tsx +205 -0
  364. package/src/session/session.context.ts +27 -0
  365. package/src/session/shell.hook.ts +19 -0
  366. package/src/window/adopt.ts +57 -0
  367. package/src/window/hooks/close-guard.hook.ts +100 -0
  368. package/src/window/hooks/target-node.hook.ts +94 -0
  369. package/src/window/hooks/views-for.hook.ts +82 -0
  370. package/src/window/hooks/window.hook.ts +18 -0
  371. package/src/window/hooks/windows.hook.ts +40 -0
  372. package/src/window/host.tsx +347 -0
  373. package/src/window/hub.ts +156 -0
  374. package/src/window/index.ts +17 -0
  375. package/src/window/lifecycle/mount.ts +134 -0
  376. package/src/window/lifecycle/states.ts +23 -0
  377. package/src/window/placement.ts +84 -0
  378. package/src/window/stage.tsx +46 -0
@@ -0,0 +1,149 @@
1
+ import type { AuthApi, Perms } from '@astrale-os/kernel-client/auth'
2
+ import type { PathLike, Ref } from '@astrale-os/kernel-client/graph'
3
+
4
+ import { rawOf } from '@astrale-os/kernel-client/graph'
5
+ /**
6
+ * useCan (API §4) — deduped standing-grant permission checks. `auth.check`-backed:
7
+ * `undefined` while the first check is in flight (or `on` is idle), then a boolean.
8
+ * Deduped per (principal, perms, nodeId) across the whole tree via the principal's
9
+ * check ledger (folded in here as the dedup substrate). Standing-grant semantics:
10
+ * the dispatched call remains the gatekeeper — this is a UI affordance, not the
11
+ * enforcement point (SPEC §6.2). A local grant/revoke invalidates the ledger.
12
+ */
13
+ import { useCallback, useMemo, useSyncExternalStore } from 'react'
14
+
15
+ import type { NodeValue, SessionView } from './act-as.context'
16
+
17
+ import { isIdleInput } from '../graph/memory/read/idle'
18
+ import { useIdentityBinding } from './act-as.context'
19
+
20
+ const NOOP_UNSUB = (): void => undefined
21
+
22
+ /** A node value carries the id/class/path triple (mirrors the memory's `isNodeValue`). */
23
+ export function isNodeValue(x: unknown): x is NodeValue {
24
+ return typeof x === 'object' && x !== null && 'id' in x && 'class' in x && 'path' in x
25
+ }
26
+
27
+ /** Normalize a `useCan` subject to a dispatchable `Ref` plus a stable dedup key. */
28
+ export function toRef(on: NodeValue | PathLike): { ref: Ref; key: string } {
29
+ if (isNodeValue(on)) return { ref: on.path as Ref, key: `@${on.id}` }
30
+ return { ref: on, key: rawOf(on) }
31
+ }
32
+
33
+ // ── check ledger (the dedup substrate) ───────────────────────────────────────
34
+
35
+ /** One deduped check — its last-known value plus the subscribers watching it. */
36
+ class CheckCell {
37
+ value: boolean | undefined = undefined
38
+ private inFlight = false
39
+ private readonly listeners = new Set<() => void>()
40
+
41
+ constructor(
42
+ private readonly auth: AuthApi,
43
+ private readonly perms: Perms,
44
+ private readonly on: Ref,
45
+ ) {}
46
+
47
+ subscribe = (onChange: () => void): (() => void) => {
48
+ this.listeners.add(onChange)
49
+ this.ensure()
50
+ return () => this.listeners.delete(onChange)
51
+ }
52
+
53
+ getSnapshot = (): boolean | undefined => this.value
54
+
55
+ /** Run the check once when the value is unknown and nothing is in flight. */
56
+ ensure(): void {
57
+ if (this.value !== undefined || this.inFlight) return
58
+ this.run()
59
+ }
60
+
61
+ /** Force a re-check (local grant/revoke on this principal). */
62
+ invalidate(): void {
63
+ if (this.listeners.size === 0) {
64
+ this.value = undefined // recompute lazily on the next subscribe
65
+ return
66
+ }
67
+ this.run()
68
+ }
69
+
70
+ private run(): void {
71
+ this.inFlight = true
72
+ void this.auth
73
+ .check({ on: this.on, perms: this.perms })
74
+ .then(
75
+ (ok) => {
76
+ this.value = ok
77
+ },
78
+ () => {
79
+ // A failed check leaves the value unknown (undefined) — never a false grant.
80
+ this.value = undefined
81
+ },
82
+ )
83
+ .finally(() => {
84
+ this.inFlight = false
85
+ for (const l of this.listeners) l()
86
+ })
87
+ }
88
+ }
89
+
90
+ /** Per-principal cell registry — dedup across the whole tree for one kernel handle. */
91
+ export class CheckLedger {
92
+ private readonly cells = new Map<string, CheckCell>()
93
+
94
+ constructor(private readonly auth: AuthApi) {}
95
+
96
+ cell(key: string, perms: Perms, on: Ref): CheckCell {
97
+ let cell = this.cells.get(key)
98
+ if (cell === undefined) {
99
+ cell = new CheckCell(this.auth, perms, on)
100
+ this.cells.set(key, cell)
101
+ }
102
+ return cell
103
+ }
104
+
105
+ /** Re-check every observed cell — conservative Tier-0 sweep after a local grant/revoke. */
106
+ invalidateAll(): void {
107
+ for (const cell of this.cells.values()) cell.invalidate()
108
+ }
109
+ }
110
+
111
+ // One ledger per bound kernel object → per principal. `session.as(cred)` mints a
112
+ // fresh view per ActAs, so acted-as principals never share a ledger with the root.
113
+ const ledgers = new WeakMap<SessionView, CheckLedger>()
114
+
115
+ export function ledgerFor(kernel: SessionView): CheckLedger {
116
+ let ledger = ledgers.get(kernel)
117
+ if (ledger === undefined) {
118
+ ledger = new CheckLedger(kernel.auth)
119
+ ledgers.set(kernel, ledger)
120
+ }
121
+ return ledger
122
+ }
123
+
124
+ // ── useCan ────────────────────────────────────────────────────────────────────
125
+
126
+ export function useCan(
127
+ perms: Perms,
128
+ on: NodeValue | PathLike | null | undefined,
129
+ ): boolean | undefined {
130
+ const binding = useIdentityBinding()
131
+ const ref = isIdleInput(on) ? null : toRef(on)
132
+
133
+ // The shared cell for (principal, perms, nodeId): identical to every other
134
+ // useCan reading the same triple, so one check serves the whole tree.
135
+ const cell = useMemo(
136
+ () =>
137
+ ref === null ? null : ledgerFor(binding.kernel).cell(`${perms}\0${ref.key}`, perms, ref.ref),
138
+ // ref is rebuilt per render; its `key` string is the stable identity.
139
+ [binding.kernel, perms, ref?.key],
140
+ )
141
+
142
+ const subscribe = useCallback(
143
+ (onChange: () => void) => (cell === null ? NOOP_UNSUB : cell.subscribe(onChange)),
144
+ [cell],
145
+ )
146
+ const getSnapshot = useCallback((): boolean | undefined => cell?.getSnapshot(), [cell])
147
+
148
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
149
+ }
@@ -0,0 +1,6 @@
1
+ // auth — API §4 (useSelf, useCan, useAuth, ActAs). See SHELL_REACT_API.md.
2
+ export { ActAs } from './act-as.context'
3
+ export type { ActAsProps } from './act-as.context'
4
+ export { useAuth } from './auth.hook'
5
+ export { useCan } from './can.hook'
6
+ export { useSelf } from './self.hook'
@@ -0,0 +1,14 @@
1
+ import type { NodeValue } from './act-as.context'
2
+
3
+ /**
4
+ * useSelf (API §4) — the caller's identity node, resolved at boot and read
5
+ * synchronously (never suspends). An <ActAs> subtree returns the acted-as
6
+ * identity. FLAG: today this is the boot-resolved whoami snapshot; it becomes a
7
+ * live memory shape once the graph plane subscribes the self node — no signature
8
+ * change.
9
+ */
10
+ import { useIdentityBinding } from './act-as.context'
11
+
12
+ export function useSelf(): NodeValue {
13
+ return useIdentityBinding().self
14
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * useSessionKernel (API §4) — the dispatch-floor resolver. Inside an `<ActAs>` it
3
+ * is the acted-as principal's bound session view; otherwise the root
4
+ * `shell.kernel`. Every dispatch floor (schema bindings, useCall/useStream, useMove,
5
+ * typed method dispatch) routes through this so a call runs as the subtree's
6
+ * principal — not the root — and its scoped memory.
7
+ */
8
+ import { useContext } from 'react'
9
+
10
+ import type { SessionView } from './act-as.context'
11
+
12
+ import { ShellContext } from '../session/session.context'
13
+ import { IdentityContext } from './act-as.context'
14
+
15
+ /** The EFFECTIVE dispatch kernel for this subtree. Throws outside a provider. */
16
+ export function useSessionKernel(): SessionView {
17
+ const binding = useContext(IdentityContext)
18
+ const runtime = useContext(ShellContext)
19
+ if (binding !== null) return binding.kernel
20
+ if (runtime === null) {
21
+ throw new Error('session hooks must be used within a <ShellProvider>')
22
+ }
23
+ return runtime.shell.kernel
24
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * The Action envelope (API §1.2) — a graph-plane primitive, not a context (rule
3
+ * 5). Holds the `Action<A,R>` type (public — it appears in every write/dispatch
4
+ * hook signature) and the `useAction` stateful-callable factory (internal): a
5
+ * render-stable callable whose pending/error/result snapshot is re-stamped each
6
+ * render, safe in deps arrays.
7
+ */
8
+ import { useCallback, useEffect, useReducer, useRef } from 'react'
9
+
10
+ /** The stateful callable every write/dispatch hook returns. */
11
+ export interface Action<A extends unknown[], R> {
12
+ /** Invoke; rejects on failure AND records it on `.error`. */
13
+ (...args: A): Promise<R>
14
+ /** ≥1 invocation in flight (the OR across concurrent calls). */
15
+ readonly pending: boolean
16
+ /** Last rejection; cleared when the next invocation starts. */
17
+ readonly error: unknown
18
+ /** Last successful resolution. */
19
+ readonly result: R | undefined
20
+ /** Clears error/result. */
21
+ reset(): void
22
+ }
23
+
24
+ /** Same shape, minus `readonly`, so the render step can stamp the snapshot on. */
25
+ interface MutableAction<A extends unknown[], R> {
26
+ (...args: A): Promise<R>
27
+ pending: boolean
28
+ error: unknown
29
+ result: R | undefined
30
+ reset(): void
31
+ }
32
+
33
+ interface ActionSnapshot<R> {
34
+ readonly pending: boolean
35
+ readonly error: unknown
36
+ readonly result: R | undefined
37
+ }
38
+
39
+ const IDLE_SNAPSHOT: ActionSnapshot<unknown> = {
40
+ pending: false,
41
+ error: undefined,
42
+ result: undefined,
43
+ }
44
+
45
+ type ActionEvent<R> =
46
+ | { readonly type: 'start' }
47
+ | { readonly type: 'resolve'; readonly value: R; readonly inFlight: number }
48
+ | { readonly type: 'reject'; readonly error: unknown; readonly inFlight: number }
49
+ | { readonly type: 'reset' }
50
+
51
+ function actionReducer<R>(state: ActionSnapshot<R>, event: ActionEvent<R>): ActionSnapshot<R> {
52
+ switch (event.type) {
53
+ case 'start':
54
+ // §1.2: a new invocation clears the error and keeps the last result.
55
+ return { pending: true, error: undefined, result: state.result }
56
+ case 'resolve':
57
+ // Latest settlement wins: a success records the result and clears error.
58
+ return { pending: event.inFlight > 0, error: undefined, result: event.value }
59
+ case 'reject':
60
+ return { pending: event.inFlight > 0, error: event.error, result: state.result }
61
+ case 'reset':
62
+ return { pending: state.pending, error: undefined, result: undefined }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Wrap an async function in the Action envelope. Concurrent invocations are
68
+ * allowed — each returns its own promise; `pending` is the OR of the in-flight
69
+ * count and `result`/`error` reflect the latest settlement. The callable keeps
70
+ * ONE identity for the hook's lifetime (safe in deps arrays); its snapshot
71
+ * fields are re-stamped each render, so reads stay current across settlements.
72
+ */
73
+ export function useAction<A extends unknown[], R>(fn: (...args: A) => Promise<R>): Action<A, R> {
74
+ // Latest-closure ref so the stable callable always dispatches the current fn.
75
+ const fnRef = useRef(fn)
76
+ fnRef.current = fn
77
+
78
+ const [snapshot, dispatch] = useReducer(actionReducer<R>, IDLE_SNAPSHOT as ActionSnapshot<R>)
79
+ const inFlight = useRef(0)
80
+ const alive = useRef(true)
81
+ useEffect(() => {
82
+ alive.current = true
83
+ return () => {
84
+ alive.current = false
85
+ }
86
+ }, [])
87
+
88
+ const call = useCallback((...args: A): Promise<R> => {
89
+ inFlight.current += 1
90
+ dispatch({ type: 'start' })
91
+ let promise: Promise<R>
92
+ try {
93
+ promise = Promise.resolve(fnRef.current(...args))
94
+ } catch (err) {
95
+ promise = Promise.reject(err)
96
+ }
97
+ void promise.then(
98
+ (value) => {
99
+ inFlight.current -= 1
100
+ if (alive.current) dispatch({ type: 'resolve', value, inFlight: inFlight.current })
101
+ },
102
+ (err: unknown) => {
103
+ inFlight.current -= 1
104
+ if (alive.current) dispatch({ type: 'reject', error: err, inFlight: inFlight.current })
105
+ },
106
+ )
107
+ return promise
108
+ }, [])
109
+
110
+ const reset = useCallback(() => dispatch({ type: 'reset' }), [])
111
+
112
+ // One render-stable callable: built once (call/reset are themselves stable),
113
+ // then its snapshot fields re-stamped every render. Re-renders are driven by
114
+ // the reducer, so the stamped pending/error/result stay fresh WITHOUT minting
115
+ // a new function identity — the documented "safe in deps arrays" guarantee.
116
+ const actionRef = useRef<MutableAction<A, R> | null>(null)
117
+ if (actionRef.current === null) {
118
+ const action = ((...args: A) => call(...args)) as MutableAction<A, R>
119
+ action.reset = reset
120
+ actionRef.current = action
121
+ }
122
+ const action = actionRef.current
123
+ action.pending = snapshot.pending
124
+ action.error = snapshot.error
125
+ action.result = snapshot.result
126
+ return action as Action<A, R>
127
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * useAsyncResource (API §1.1) — a one-shot async read projected into the §1 read
3
+ * envelope (`ReadState`): the non-memory sibling of `useRead`, stale-guarded and
4
+ * `refetch`-able, with a `null` loader as idle. Shared by `application/` and the
5
+ * window `useViewsFor` dispatch; homed here (below both) to keep rule 9 acyclic.
6
+ */
7
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
8
+
9
+ import type { ReadState } from './memory/read/envelope'
10
+
11
+ export interface AsyncResource<T> extends ReadState {
12
+ readonly data: T
13
+ }
14
+
15
+ interface AsyncState<T> {
16
+ readonly data: T
17
+ readonly pending: boolean
18
+ readonly error: unknown
19
+ }
20
+
21
+ /**
22
+ * Subscribe a component to `load()`. `null` loader → idle. The loader runs on
23
+ * mount and whenever `deps` change (stale responses discarded); last-good data is
24
+ * retained through a pending refetch or a failure (§1.1).
25
+ */
26
+ export function useAsyncResource<T>(
27
+ load: (() => Promise<T>) | null,
28
+ empty: T,
29
+ deps: readonly unknown[],
30
+ ): AsyncResource<T> {
31
+ const [state, setState] = useState<AsyncState<T>>({
32
+ data: empty,
33
+ pending: load !== null,
34
+ error: undefined,
35
+ })
36
+
37
+ const loadRef = useRef(load)
38
+ loadRef.current = load
39
+ const emptyRef = useRef(empty)
40
+ emptyRef.current = empty
41
+ const seq = useRef(0)
42
+ const alive = useRef(true)
43
+ useEffect(() => {
44
+ alive.current = true
45
+ return () => {
46
+ alive.current = false
47
+ }
48
+ }, [])
49
+
50
+ const refetch = useCallback((): Promise<void> => {
51
+ const run = loadRef.current
52
+ if (run === null) {
53
+ setState({ data: emptyRef.current, pending: false, error: undefined })
54
+ return Promise.resolve()
55
+ }
56
+ const my = ++seq.current
57
+ setState((s) => ({ data: s.data, pending: true, error: undefined }))
58
+ return run().then(
59
+ (data) => {
60
+ if (alive.current && my === seq.current)
61
+ setState({ data, pending: false, error: undefined })
62
+ },
63
+ (error: unknown) => {
64
+ if (alive.current && my === seq.current) {
65
+ setState((s) => ({ data: s.data, pending: false, error }))
66
+ }
67
+ },
68
+ )
69
+ }, [])
70
+
71
+ useEffect(() => {
72
+ void refetch()
73
+ // eslint-disable-next-line react-hooks/exhaustive-deps
74
+ }, deps)
75
+
76
+ return useMemo(
77
+ () => ({
78
+ data: state.data,
79
+ pending: state.pending,
80
+ error: state.error,
81
+ live: false,
82
+ seq: 0,
83
+ refetch,
84
+ }),
85
+ [state.data, state.pending, state.error, refetch],
86
+ )
87
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * useCall (API §7) — unary domain-function dispatch over `kernel.call`, in the
3
+ * Action envelope. No result caching, no liveness — domain function reads are not
4
+ * structurally invalidatable (FRONTEND §5.7); a consumer that needs freshness
5
+ * owns its own refetch policy. Dispatch is a graph operation (rule 2).
6
+ */
7
+ import type { CallFns, FnMap } from '@astrale-os/kernel-client'
8
+
9
+ import type { Action } from '../action'
10
+
11
+ import { useSessionKernel } from '../../auth/session-kernel'
12
+ import { useAction } from '../action'
13
+
14
+ export interface CallOptions {
15
+ /** Per-call timeout (ms), forwarded to the kernel dispatch. */
16
+ timeout?: number
17
+ /** Side-channel notified on rejection (the Action also records `.error`). */
18
+ onError?(error: unknown): void
19
+ }
20
+
21
+ /**
22
+ * Unary domain-function dispatch as an {@link Action}. `method` is a typed FnMap
23
+ * `call` path or any string (the untyped floor); `null` yields an idle action
24
+ * that rejects if invoked. Params/result are typed from the FnMap entry.
25
+ */
26
+ export function useCall<K extends CallFns<FnMap>>(
27
+ method: K | null,
28
+ opts?: CallOptions,
29
+ ): Action<[FnMap[K]['params']], FnMap[K]['result']> {
30
+ const kernel = useSessionKernel()
31
+ // A fresh closure per render captures the current method/opts; `useAction`
32
+ // keeps ONE stable callable and always dispatches this latest closure.
33
+ return useAction(async (params: FnMap[K]['params']): Promise<FnMap[K]['result']> => {
34
+ if (method === null) throw new Error('useCall: method is null (idle action invoked)')
35
+ try {
36
+ return await kernel.call(method, params, { timeout: opts?.timeout })
37
+ } catch (error) {
38
+ opts?.onError?.(error)
39
+ throw error
40
+ }
41
+ })
42
+ }
@@ -0,0 +1,200 @@
1
+ import type { FnMap, StreamFns } from '@astrale-os/kernel-client'
2
+
3
+ /**
4
+ * useStream (API §7) — streaming domain-function dispatch over `kernel.stream`,
5
+ * an AsyncIterable driven by an explicit start/cancel handle. A superseding
6
+ * `start` (or unmount) closes the prior run and discards its late chunks/errors;
7
+ * never live. Dispatch is a graph operation (rule 2).
8
+ */
9
+ import { useCallback, useEffect, useReducer, useRef } from 'react'
10
+
11
+ import { useSessionKernel } from '../../auth/session-kernel'
12
+
13
+ export interface StreamOptions<T> {
14
+ /** Notified per chunk as it arrives (in addition to `chunks` accumulation). */
15
+ onChunk?(chunk: T): void
16
+ }
17
+
18
+ export interface StreamHandle<T> {
19
+ /** Begin a run: resets chunks, then resolves when the stream ends / rejects on failure. */
20
+ start(params: unknown): Promise<void>
21
+ /** Chunks accumulated in the current run; reset on the next `start`. */
22
+ readonly chunks: readonly T[]
23
+ /** The most recent chunk, or null before the first. */
24
+ readonly last: T | null
25
+ /** A run is in flight. */
26
+ readonly streaming: boolean
27
+ /** Last run failure; retained until the next `start`. */
28
+ readonly error: unknown
29
+ /** Close the stream (returns the iterator); a later `start` begins fresh. */
30
+ cancel(): void
31
+ }
32
+
33
+ interface StreamSnapshot<T> {
34
+ readonly chunks: readonly T[]
35
+ readonly last: T | null
36
+ readonly streaming: boolean
37
+ readonly error: unknown
38
+ }
39
+
40
+ const IDLE_STREAM: StreamSnapshot<unknown> = {
41
+ chunks: [],
42
+ last: null,
43
+ streaming: false,
44
+ error: undefined,
45
+ }
46
+
47
+ type StreamEvent<T> =
48
+ | { readonly type: 'start' }
49
+ | { readonly type: 'chunk'; readonly value: T }
50
+ | { readonly type: 'end' }
51
+ | { readonly type: 'error'; readonly error: unknown }
52
+ | { readonly type: 'cancel' }
53
+
54
+ function streamReducer<T>(state: StreamSnapshot<T>, event: StreamEvent<T>): StreamSnapshot<T> {
55
+ switch (event.type) {
56
+ case 'start':
57
+ return { chunks: [], last: null, streaming: true, error: undefined }
58
+ case 'chunk':
59
+ return {
60
+ chunks: [...state.chunks, event.value],
61
+ last: event.value,
62
+ streaming: true,
63
+ error: undefined,
64
+ }
65
+ case 'end':
66
+ return { ...state, streaming: false }
67
+ case 'error':
68
+ // Chunks accumulated before the failure are preserved.
69
+ return { ...state, streaming: false, error: event.error }
70
+ case 'cancel':
71
+ return { ...state, streaming: false }
72
+ }
73
+ }
74
+
75
+ /** Same shape, minus `readonly`, so the render step can re-stamp the snapshot. */
76
+ interface MutableStreamHandle<T> {
77
+ start(params: unknown): Promise<void>
78
+ chunks: readonly T[]
79
+ last: T | null
80
+ streaming: boolean
81
+ error: unknown
82
+ cancel(): void
83
+ }
84
+
85
+ interface StreamRun<T> {
86
+ readonly controller: AbortController
87
+ iterator: AsyncIterator<T> | null
88
+ }
89
+
90
+ /**
91
+ * Streaming domain-function dispatch. `start(params)` opens a run over
92
+ * `kernel.stream`; chunks accumulate on `chunks`; `cancel()` closes the iterator
93
+ * and a later `start` begins fresh. A superseding `start` (or unmount) closes the
94
+ * prior run and discards its late chunks/errors — only the current run drives
95
+ * state. Never live; the handle keeps one identity for the hook's lifetime.
96
+ */
97
+ export function useStream<K extends StreamFns<FnMap>>(
98
+ method: K | null,
99
+ opts?: StreamOptions<FnMap[K]['result']>,
100
+ ): StreamHandle<FnMap[K]['result']> {
101
+ type T = FnMap[K]['result']
102
+ const kernel = useSessionKernel()
103
+
104
+ // Latest-closure refs so the stable start/cancel dispatch the current inputs.
105
+ const kernelRef = useRef(kernel)
106
+ kernelRef.current = kernel
107
+ const methodRef = useRef(method)
108
+ methodRef.current = method
109
+ const optsRef = useRef(opts)
110
+ optsRef.current = opts
111
+
112
+ const [snapshot, dispatch] = useReducer(streamReducer<T>, IDLE_STREAM as StreamSnapshot<T>)
113
+
114
+ // `runId` supersession token; only the run whose id is still current drives state.
115
+ const runIdRef = useRef(0)
116
+ const currentRef = useRef<StreamRun<T> | null>(null)
117
+ const alive = useRef(true)
118
+
119
+ const stop = useCallback(() => {
120
+ const run = currentRef.current
121
+ if (run !== null) {
122
+ run.controller.abort()
123
+ void run.iterator?.return?.(undefined)
124
+ currentRef.current = null
125
+ }
126
+ }, [])
127
+
128
+ useEffect(() => {
129
+ alive.current = true
130
+ return () => {
131
+ alive.current = false
132
+ stop()
133
+ }
134
+ }, [stop])
135
+
136
+ const cancel = useCallback(() => {
137
+ runIdRef.current += 1
138
+ stop()
139
+ if (alive.current) dispatch({ type: 'cancel' })
140
+ }, [stop])
141
+
142
+ const start = useCallback(
143
+ (params: unknown): Promise<void> => {
144
+ const method_ = methodRef.current
145
+ if (method_ === null) {
146
+ return Promise.reject(new Error('useStream: method is null (idle handle started)'))
147
+ }
148
+ runIdRef.current += 1
149
+ const runId = runIdRef.current
150
+ stop() // close any prior run before opening the new one
151
+ dispatch({ type: 'start' })
152
+
153
+ const controller = new AbortController()
154
+ const run: StreamRun<T> = { controller, iterator: null }
155
+ currentRef.current = run
156
+ const current = (): boolean => runIdRef.current === runId
157
+
158
+ return (async () => {
159
+ try {
160
+ const iterable = await kernelRef.current.stream(method_, params, {
161
+ signal: controller.signal,
162
+ })
163
+ if (!current()) return
164
+ const iterator = iterable[Symbol.asyncIterator]()
165
+ run.iterator = iterator
166
+ for (;;) {
167
+ const next = await iterator.next()
168
+ if (!current()) return
169
+ if (next.done === true) break
170
+ if (alive.current) dispatch({ type: 'chunk', value: next.value })
171
+ optsRef.current?.onChunk?.(next.value)
172
+ }
173
+ if (current()) {
174
+ currentRef.current = null
175
+ if (alive.current) dispatch({ type: 'end' })
176
+ }
177
+ } catch (error) {
178
+ if (!current()) return // a superseded/cancelled run's failure is discarded
179
+ currentRef.current = null
180
+ if (alive.current) dispatch({ type: 'error', error })
181
+ throw error
182
+ }
183
+ })()
184
+ },
185
+ [stop],
186
+ )
187
+
188
+ // One render-stable handle: built once (start/cancel are stable), then its
189
+ // snapshot fields re-stamped each render so reads stay current across chunks.
190
+ const handleRef = useRef<MutableStreamHandle<T> | null>(null)
191
+ if (handleRef.current === null) {
192
+ handleRef.current = { start, cancel } as MutableStreamHandle<T>
193
+ }
194
+ const handle = handleRef.current
195
+ handle.chunks = snapshot.chunks
196
+ handle.last = snapshot.last
197
+ handle.streaming = snapshot.streaming
198
+ handle.error = snapshot.error
199
+ return handle as StreamHandle<T>
200
+ }
@@ -0,0 +1,14 @@
1
+ import type { GraphMemory } from '@astrale-os/kernel-client/store'
2
+
3
+ /**
4
+ * GraphContext (ex MemoryContext) — the root of the graph plane. Seats the base
5
+ * (default-scope) memory the provider builds; the SAME handle the graph/schema/
6
+ * nav hooks read through `memory/scope.ts`, and selection's liveness source.
7
+ * Under `<ActAs>` the read hooks re-scope via `forScope`; selection reads the
8
+ * base scope here.
9
+ */
10
+ import { createContext } from 'react'
11
+
12
+ export type { GraphMemory }
13
+ export const GraphContext = createContext<GraphMemory | null>(null)
14
+ GraphContext.displayName = 'AstraleGraphContext'