@astrale-os/shell-react 0.2.9 → 0.2.10-beta.2

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 (339) hide show
  1. package/dist/application/apps/app-host.d.ts +4 -15
  2. package/dist/application/apps/app-host.d.ts.map +1 -1
  3. package/dist/application/apps/app-host.js +1 -1
  4. package/dist/application/apps/apps.hook.d.ts +7 -4
  5. package/dist/application/apps/apps.hook.d.ts.map +1 -1
  6. package/dist/application/apps/apps.hook.js +10 -2
  7. package/dist/application/apps/domains.hook.d.ts +1 -1
  8. package/dist/application/apps/domains.hook.d.ts.map +1 -1
  9. package/dist/application/apps/domains.hook.js +1 -1
  10. package/dist/application/apps/open-app.hook.d.ts +2 -3
  11. package/dist/application/apps/open-app.hook.d.ts.map +1 -1
  12. package/dist/application/index.d.ts +2 -4
  13. package/dist/application/index.d.ts.map +1 -1
  14. package/dist/application/index.js +2 -3
  15. package/dist/application/spaces/space.hook.d.ts +2 -2
  16. package/dist/application/spaces/space.hook.d.ts.map +1 -1
  17. package/dist/application/spaces/space.hook.js +2 -2
  18. package/dist/application/spaces/spaces.hook.d.ts +2 -3
  19. package/dist/application/spaces/spaces.hook.d.ts.map +1 -1
  20. package/dist/application/spaces/spaces.hook.js +10 -3
  21. package/dist/auth/act-as.context.d.ts +15 -32
  22. package/dist/auth/act-as.context.d.ts.map +1 -1
  23. package/dist/auth/act-as.context.js +22 -66
  24. package/dist/auth/auth.hook.d.ts +1 -0
  25. package/dist/auth/auth.hook.d.ts.map +1 -1
  26. package/dist/auth/auth.hook.js +2 -22
  27. package/dist/auth/can.hook.d.ts +10 -38
  28. package/dist/auth/can.hook.d.ts.map +1 -1
  29. package/dist/auth/can.hook.js +35 -110
  30. package/dist/auth/index.d.ts +1 -0
  31. package/dist/auth/index.d.ts.map +1 -1
  32. package/dist/auth/self.hook.d.ts +2 -2
  33. package/dist/auth/self.hook.d.ts.map +1 -1
  34. package/dist/auth/self.hook.js +0 -7
  35. package/dist/auth/session-kernel.d.ts +2 -3
  36. package/dist/auth/session-kernel.d.ts.map +1 -1
  37. package/dist/auth/session-kernel.js +4 -13
  38. package/dist/components/boundaries.d.ts +47 -0
  39. package/dist/components/boundaries.d.ts.map +1 -0
  40. package/dist/components/boundaries.js +55 -0
  41. package/dist/components/can.d.ts +11 -0
  42. package/dist/components/can.d.ts.map +1 -0
  43. package/dist/components/can.js +5 -0
  44. package/dist/components/index.d.ts +7 -0
  45. package/dist/components/index.d.ts.map +1 -0
  46. package/dist/components/index.js +4 -0
  47. package/dist/components/mount-domain-frontend.d.ts +22 -0
  48. package/dist/components/mount-domain-frontend.d.ts.map +1 -0
  49. package/dist/components/mount-domain-frontend.js +85 -0
  50. package/dist/components/view-app.d.ts +16 -0
  51. package/dist/components/view-app.d.ts.map +1 -0
  52. package/dist/components/view-app.js +16 -0
  53. package/dist/graph/call/call.hook.d.ts +8 -18
  54. package/dist/graph/call/call.hook.d.ts.map +1 -1
  55. package/dist/graph/call/call.hook.js +7 -13
  56. package/dist/graph/call/stream.hook.d.ts +12 -20
  57. package/dist/graph/call/stream.hook.d.ts.map +1 -1
  58. package/dist/graph/call/stream.hook.js +55 -111
  59. package/dist/graph/graph.context.d.ts +4 -3
  60. package/dist/graph/graph.context.d.ts.map +1 -1
  61. package/dist/graph/graph.context.js +2 -8
  62. package/dist/graph/guards.error.d.ts +7 -8
  63. package/dist/graph/guards.error.d.ts.map +1 -1
  64. package/dist/graph/guards.error.js +11 -7
  65. package/dist/graph/index.d.ts +8 -16
  66. package/dist/graph/index.d.ts.map +1 -1
  67. package/dist/graph/index.js +3 -7
  68. package/dist/graph/memory/read/envelope.d.ts +22 -54
  69. package/dist/graph/memory/read/envelope.d.ts.map +1 -1
  70. package/dist/graph/memory/read/envelope.js +26 -20
  71. package/dist/graph/memory/read/idle.d.ts +1 -15
  72. package/dist/graph/memory/read/idle.d.ts.map +1 -1
  73. package/dist/graph/memory/read/idle.js +4 -13
  74. package/dist/graph/query-definition.hook.d.ts +11 -0
  75. package/dist/graph/query-definition.hook.d.ts.map +1 -0
  76. package/dist/graph/query-definition.hook.js +205 -0
  77. package/dist/graph/resource.d.ts +34 -0
  78. package/dist/graph/resource.d.ts.map +1 -0
  79. package/dist/graph/resource.js +244 -0
  80. package/dist/graph/store.d.ts +25 -0
  81. package/dist/graph/store.d.ts.map +1 -0
  82. package/dist/graph/store.js +69 -0
  83. package/dist/host/actions.hook.d.ts +34 -0
  84. package/dist/host/actions.hook.d.ts.map +1 -0
  85. package/dist/host/actions.hook.js +125 -0
  86. package/dist/host/components.d.ts +15 -0
  87. package/dist/host/components.d.ts.map +1 -0
  88. package/dist/host/components.js +14 -0
  89. package/dist/host/index.d.ts +5 -0
  90. package/dist/host/index.d.ts.map +1 -0
  91. package/dist/host/index.js +3 -0
  92. package/dist/index.d.ts +26 -37
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +20 -31
  95. package/dist/intents/envelope.d.ts +7 -19
  96. package/dist/intents/envelope.d.ts.map +1 -1
  97. package/dist/intents/envelope.js +2 -3
  98. package/dist/intents/send-intent.hook.d.ts +5 -7
  99. package/dist/intents/send-intent.hook.d.ts.map +1 -1
  100. package/dist/intents/send-intent.hook.js +17 -75
  101. package/dist/navigation/adapters/adapter.d.ts +5 -0
  102. package/dist/navigation/adapters/adapter.d.ts.map +1 -1
  103. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -1
  104. package/dist/navigation/adapters/memory.adapter.js +4 -0
  105. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -1
  106. package/dist/navigation/adapters/url.adapter.js +57 -10
  107. package/dist/navigation/codec.d.ts +60 -0
  108. package/dist/navigation/codec.d.ts.map +1 -0
  109. package/dist/navigation/codec.js +121 -0
  110. package/dist/navigation/history.d.ts +11 -9
  111. package/dist/navigation/history.d.ts.map +1 -1
  112. package/dist/navigation/history.js +6 -8
  113. package/dist/navigation/hooks/ancestors.hook.d.ts +5 -7
  114. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -1
  115. package/dist/navigation/hooks/ancestors.hook.js +7 -51
  116. package/dist/navigation/hooks/location.hook.d.ts.map +1 -1
  117. package/dist/navigation/hooks/location.hook.js +4 -6
  118. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -1
  119. package/dist/navigation/hooks/scope.hook.js +4 -6
  120. package/dist/navigation/index.d.ts +4 -2
  121. package/dist/navigation/index.d.ts.map +1 -1
  122. package/dist/navigation/index.js +2 -1
  123. package/dist/navigation/link.d.ts +12 -0
  124. package/dist/navigation/link.d.ts.map +1 -1
  125. package/dist/navigation/link.js +13 -2
  126. package/dist/navigation/location.d.ts +13 -40
  127. package/dist/navigation/location.d.ts.map +1 -1
  128. package/dist/navigation/location.js +14 -76
  129. package/dist/navigation/scope.context.d.ts +17 -26
  130. package/dist/navigation/scope.context.d.ts.map +1 -1
  131. package/dist/navigation/scope.context.js +140 -108
  132. package/dist/navigation/snapshot.d.ts +14 -19
  133. package/dist/navigation/snapshot.d.ts.map +1 -1
  134. package/dist/navigation/snapshot.js +48 -44
  135. package/dist/schema/action.hook.d.ts +44 -0
  136. package/dist/schema/action.hook.d.ts.map +1 -0
  137. package/dist/schema/action.hook.js +72 -0
  138. package/dist/schema/domain.hook.d.ts +12 -7
  139. package/dist/schema/domain.hook.d.ts.map +1 -1
  140. package/dist/schema/domain.hook.js +101 -4
  141. package/dist/schema/index.d.ts +9 -8
  142. package/dist/schema/index.d.ts.map +1 -1
  143. package/dist/schema/index.js +5 -7
  144. package/dist/schema/kernel.hook.d.ts +4 -9
  145. package/dist/schema/kernel.hook.d.ts.map +1 -1
  146. package/dist/schema/kernel.hook.js +4 -6
  147. package/dist/schema/mutation.hook.d.ts +9 -0
  148. package/dist/schema/mutation.hook.d.ts.map +1 -0
  149. package/dist/schema/mutation.hook.js +41 -0
  150. package/dist/schema/object.hook.d.ts +16 -0
  151. package/dist/schema/object.hook.d.ts.map +1 -0
  152. package/dist/schema/object.hook.js +46 -0
  153. package/dist/schema/relation.hook.d.ts +46 -0
  154. package/dist/schema/relation.hook.d.ts.map +1 -0
  155. package/dist/schema/relation.hook.js +136 -0
  156. package/dist/selection/live.d.ts +9 -17
  157. package/dist/selection/live.d.ts.map +1 -1
  158. package/dist/selection/live.js +65 -112
  159. package/dist/selection/scope.context.js +5 -4
  160. package/dist/session/boot/boot.d.ts +1 -13
  161. package/dist/session/boot/boot.d.ts.map +1 -1
  162. package/dist/session/boot/boot.js +9 -27
  163. package/dist/session/index.d.ts +1 -0
  164. package/dist/session/index.d.ts.map +1 -1
  165. package/dist/session/kernel-instance.hook.d.ts +1 -4
  166. package/dist/session/kernel-instance.hook.d.ts.map +1 -1
  167. package/dist/session/kernel-instance.hook.js +1 -5
  168. package/dist/session/provider.d.ts +9 -23
  169. package/dist/session/provider.d.ts.map +1 -1
  170. package/dist/session/provider.js +29 -75
  171. package/dist/session/session.context.d.ts +5 -0
  172. package/dist/session/session.context.d.ts.map +1 -1
  173. package/dist/window/adopt.d.ts +0 -1
  174. package/dist/window/adopt.d.ts.map +1 -1
  175. package/dist/window/hooks/target-node.hook.d.ts +10 -6
  176. package/dist/window/hooks/target-node.hook.d.ts.map +1 -1
  177. package/dist/window/hooks/target-node.hook.js +14 -72
  178. package/dist/window/hooks/views-for.hook.d.ts +4 -3
  179. package/dist/window/hooks/views-for.hook.d.ts.map +1 -1
  180. package/dist/window/hooks/views-for.hook.js +6 -22
  181. package/dist/window/hooks/windows.hook.d.ts.map +1 -1
  182. package/dist/window/hooks/windows.hook.js +3 -4
  183. package/dist/window/host.d.ts +9 -11
  184. package/dist/window/host.d.ts.map +1 -1
  185. package/dist/window/host.js +26 -62
  186. package/dist/window/hub.d.ts +2 -0
  187. package/dist/window/hub.d.ts.map +1 -1
  188. package/dist/window/hub.js +33 -0
  189. package/dist/window/index.d.ts +2 -2
  190. package/dist/window/index.d.ts.map +1 -1
  191. package/dist/window/index.js +2 -2
  192. package/dist/window/lifecycle/mount.d.ts +5 -44
  193. package/dist/window/lifecycle/mount.d.ts.map +1 -1
  194. package/dist/window/lifecycle/mount.js +14 -120
  195. package/dist/window/lifecycle/states.d.ts +0 -5
  196. package/dist/window/lifecycle/states.d.ts.map +1 -1
  197. package/package.json +9 -8
  198. package/src/application/apps/app-host.tsx +11 -15
  199. package/src/application/apps/apps.hook.ts +13 -6
  200. package/src/application/apps/domains.hook.ts +1 -1
  201. package/src/application/apps/open-app.hook.ts +3 -4
  202. package/src/application/index.ts +4 -6
  203. package/src/application/spaces/space.hook.ts +5 -6
  204. package/src/application/spaces/spaces.hook.ts +14 -9
  205. package/src/auth/act-as.context.tsx +37 -91
  206. package/src/auth/auth.hook.ts +2 -30
  207. package/src/auth/can.hook.ts +44 -135
  208. package/src/auth/index.ts +1 -0
  209. package/src/auth/self.hook.ts +2 -9
  210. package/src/auth/session-kernel.ts +6 -16
  211. package/src/components/boundaries.tsx +113 -0
  212. package/src/components/can.tsx +16 -0
  213. package/src/components/index.ts +14 -0
  214. package/src/components/mount-domain-frontend.tsx +158 -0
  215. package/src/components/view-app.tsx +49 -0
  216. package/src/graph/call/call.hook.ts +22 -27
  217. package/src/graph/call/stream.hook.ts +66 -143
  218. package/src/graph/graph.context.ts +6 -11
  219. package/src/graph/guards.error.ts +17 -55
  220. package/src/graph/index.ts +23 -17
  221. package/src/graph/memory/read/envelope.ts +40 -65
  222. package/src/graph/memory/read/idle.ts +5 -18
  223. package/src/graph/query-definition.hook.ts +270 -0
  224. package/src/graph/resource.ts +358 -0
  225. package/src/graph/store.ts +97 -0
  226. package/src/host/actions.hook.ts +165 -0
  227. package/src/host/components.tsx +29 -0
  228. package/src/host/index.ts +17 -0
  229. package/src/index.ts +136 -124
  230. package/src/intents/envelope.ts +7 -23
  231. package/src/intents/send-intent.hook.ts +24 -79
  232. package/src/navigation/adapters/adapter.ts +3 -0
  233. package/src/navigation/adapters/memory.adapter.ts +4 -0
  234. package/src/navigation/adapters/url.adapter.ts +48 -8
  235. package/src/navigation/codec.ts +190 -0
  236. package/src/navigation/history.ts +20 -11
  237. package/src/navigation/hooks/ancestors.hook.ts +10 -60
  238. package/src/navigation/hooks/location.hook.ts +4 -6
  239. package/src/navigation/hooks/scope.hook.ts +4 -6
  240. package/src/navigation/index.ts +18 -2
  241. package/src/navigation/link.tsx +27 -2
  242. package/src/navigation/location.ts +26 -93
  243. package/src/navigation/scope.context.tsx +151 -136
  244. package/src/navigation/snapshot.ts +69 -50
  245. package/src/schema/action.hook.ts +146 -0
  246. package/src/schema/domain.hook.ts +127 -11
  247. package/src/schema/index.ts +9 -30
  248. package/src/schema/kernel.hook.ts +6 -13
  249. package/src/schema/mutation.hook.ts +63 -0
  250. package/src/schema/object.hook.ts +85 -0
  251. package/src/schema/relation.hook.ts +253 -0
  252. package/src/selection/live.ts +75 -125
  253. package/src/selection/scope.context.tsx +4 -4
  254. package/src/session/boot/boot.ts +8 -33
  255. package/src/session/index.ts +1 -0
  256. package/src/session/kernel-instance.hook.ts +2 -11
  257. package/src/session/provider.tsx +34 -103
  258. package/src/session/session.context.ts +6 -0
  259. package/src/window/adopt.ts +0 -1
  260. package/src/window/hooks/target-node.hook.ts +20 -85
  261. package/src/window/hooks/views-for.hook.ts +12 -25
  262. package/src/window/hooks/windows.hook.ts +3 -4
  263. package/src/window/host.tsx +35 -77
  264. package/src/window/hub.ts +33 -0
  265. package/src/window/index.ts +3 -3
  266. package/src/window/lifecycle/mount.ts +15 -160
  267. package/src/window/lifecycle/states.ts +1 -5
  268. package/dist/application/roles.hook.d.ts +0 -7
  269. package/dist/application/roles.hook.d.ts.map +0 -1
  270. package/dist/application/roles.hook.js +0 -23
  271. package/dist/graph/memory/base.d.ts +0 -21
  272. package/dist/graph/memory/base.d.ts.map +0 -1
  273. package/dist/graph/memory/base.js +0 -33
  274. package/dist/graph/memory/read/read.d.ts +0 -9
  275. package/dist/graph/memory/read/read.d.ts.map +0 -1
  276. package/dist/graph/memory/read/read.js +0 -87
  277. package/dist/graph/memory/scope.d.ts +0 -7
  278. package/dist/graph/memory/scope.d.ts.map +0 -1
  279. package/dist/graph/memory/scope.js +0 -25
  280. package/dist/graph/mutate/move.hook.d.ts +0 -7
  281. package/dist/graph/mutate/move.hook.d.ts.map +0 -1
  282. package/dist/graph/mutate/move.hook.js +0 -49
  283. package/dist/graph/mutate/mutate.hook.d.ts +0 -17
  284. package/dist/graph/mutate/mutate.hook.d.ts.map +0 -1
  285. package/dist/graph/mutate/mutate.hook.js +0 -45
  286. package/dist/graph/query/children.hook.d.ts +0 -29
  287. package/dist/graph/query/children.hook.d.ts.map +0 -1
  288. package/dist/graph/query/children.hook.js +0 -64
  289. package/dist/graph/query/graph.hook.d.ts +0 -9
  290. package/dist/graph/query/graph.hook.d.ts.map +0 -1
  291. package/dist/graph/query/graph.hook.js +0 -5
  292. package/dist/graph/query/neighbors.hook.d.ts +0 -7
  293. package/dist/graph/query/neighbors.hook.d.ts.map +0 -1
  294. package/dist/graph/query/neighbors.hook.js +0 -104
  295. package/dist/graph/query/node.hook.d.ts +0 -14
  296. package/dist/graph/query/node.hook.d.ts.map +0 -1
  297. package/dist/graph/query/node.hook.js +0 -43
  298. package/dist/graph/query/query.hook.d.ts +0 -14
  299. package/dist/graph/query/query.hook.d.ts.map +0 -1
  300. package/dist/graph/query/query.hook.js +0 -23
  301. package/dist/schema/bind-read.d.ts +0 -37
  302. package/dist/schema/bind-read.d.ts.map +0 -1
  303. package/dist/schema/bind-read.js +0 -205
  304. package/dist/schema/binding.d.ts +0 -21
  305. package/dist/schema/binding.d.ts.map +0 -1
  306. package/dist/schema/binding.js +0 -99
  307. package/dist/schema/function.hook.d.ts +0 -9
  308. package/dist/schema/function.hook.d.ts.map +0 -1
  309. package/dist/schema/function.hook.js +0 -40
  310. package/dist/schema/method.hook.d.ts +0 -14
  311. package/dist/schema/method.hook.d.ts.map +0 -1
  312. package/dist/schema/method.hook.js +0 -49
  313. package/dist/schema/tag.d.ts +0 -11
  314. package/dist/schema/tag.d.ts.map +0 -1
  315. package/dist/schema/tag.js +0 -11
  316. package/dist/schema/types.d.ts +0 -73
  317. package/dist/schema/types.d.ts.map +0 -1
  318. package/dist/schema/types.js +0 -1
  319. package/dist/schema/write.d.ts +0 -16
  320. package/dist/schema/write.d.ts.map +0 -1
  321. package/dist/schema/write.js +0 -56
  322. package/src/application/roles.hook.ts +0 -41
  323. package/src/graph/memory/base.ts +0 -50
  324. package/src/graph/memory/read/read.ts +0 -103
  325. package/src/graph/memory/scope.ts +0 -29
  326. package/src/graph/mutate/move.hook.ts +0 -70
  327. package/src/graph/mutate/mutate.hook.ts +0 -69
  328. package/src/graph/query/children.hook.ts +0 -136
  329. package/src/graph/query/graph.hook.ts +0 -13
  330. package/src/graph/query/neighbors.hook.ts +0 -159
  331. package/src/graph/query/node.hook.ts +0 -83
  332. package/src/graph/query/query.hook.ts +0 -42
  333. package/src/schema/bind-read.ts +0 -319
  334. package/src/schema/binding.ts +0 -146
  335. package/src/schema/function.hook.ts +0 -68
  336. package/src/schema/method.hook.ts +0 -103
  337. package/src/schema/tag.ts +0 -19
  338. package/src/schema/types.ts +0 -150
  339. package/src/schema/write.ts +0 -96
@@ -0,0 +1,270 @@
1
+ import type { GraphStore, StoreQueryResponse, StoreSnapshot } from '@astrale-os/kernel-client/store'
2
+ import type {
3
+ QueryAstExecutionOptions,
4
+ QueryDefinition,
5
+ QueryValueBinder,
6
+ } from '@astrale-os/sdk/query'
7
+ import type { QueryAST } from '@astrale-os/sdk/query'
8
+ import type { DomainBinding } from '@astrale-os/shell'
9
+
10
+ import { bindQueries } from '@astrale-os/sdk/query'
11
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
12
+
13
+ import type { GraphResource, GraphResourceState } from './resource.js'
14
+
15
+ import { useDynamicDomain } from '../schema/domain.hook.js'
16
+ import { retainRefreshScheduler } from './resource.js'
17
+ import { useGraphStore } from './store.js'
18
+
19
+ export interface QueryResourceOptions {
20
+ readonly keepPrevious?: boolean
21
+ readonly refreshInterval?: number
22
+ readonly refreshWhen?: 'always' | 'visible'
23
+ }
24
+
25
+ interface TouchedShape {
26
+ readonly ast: QueryAST
27
+ readonly options: QueryAstExecutionOptions
28
+ }
29
+
30
+ interface DefinitionSnapshot<Output> {
31
+ readonly state: GraphResourceState
32
+ readonly data?: Output
33
+ readonly error?: unknown
34
+ }
35
+
36
+ class DefinitionObserver<Input, Output, Query extends QueryAST> {
37
+ private readonly listeners = new Set<() => void>()
38
+ private subscriptions: readonly (() => void)[] = Object.freeze([])
39
+ private touched: readonly TouchedShape[] = Object.freeze([])
40
+ private current: DefinitionSnapshot<Output> = Object.freeze({ state: 'loading' })
41
+ private generation = 0
42
+ private pending: Promise<void> | null = null
43
+ private started = false
44
+ private rerunQueued = false
45
+
46
+ constructor(
47
+ private readonly store: GraphStore,
48
+ private readonly binding: DomainBinding,
49
+ private readonly definition: QueryDefinition<Input, Output, Query>,
50
+ private readonly input: Input,
51
+ ) {}
52
+
53
+ subscribe = (listener: () => void): (() => void) => {
54
+ this.listeners.add(listener)
55
+ return () => this.listeners.delete(listener)
56
+ }
57
+
58
+ getSnapshot = (): DefinitionSnapshot<Output> => this.current
59
+
60
+ start(): () => void {
61
+ this.started = true
62
+ void this.run('cached')
63
+ return () => {
64
+ this.started = false
65
+ this.generation += 1
66
+ this.subscriptions.forEach((unsubscribe) => unsubscribe())
67
+ this.subscriptions = Object.freeze([])
68
+ }
69
+ }
70
+
71
+ refresh = async (): Promise<void> => {
72
+ await this.run('refresh')
73
+ }
74
+
75
+ private run(freshness: 'cached' | 'refresh'): Promise<void> {
76
+ if (this.pending !== null && freshness === 'cached') return this.pending
77
+ const generation = ++this.generation
78
+ const touched: TouchedShape[] = []
79
+ this.update({
80
+ state: this.current.data === undefined ? 'loading' : 'refreshing',
81
+ ...(this.current.data === undefined ? {} : { data: this.current.data }),
82
+ })
83
+ const execute = async <Ast extends QueryAST>(
84
+ ast: Ast,
85
+ options: QueryAstExecutionOptions,
86
+ ): Promise<StoreQueryResponse<Ast>> => {
87
+ touched.push(Object.freeze({ ast, options }))
88
+ return this.store.read(ast, { ...options, freshness })
89
+ }
90
+ const bind = this.binding.$.bind as unknown as QueryValueBinder
91
+ const query = bindQueries(execute, { bind })
92
+ const running = query(this.definition, this.input)
93
+ .then((data) => {
94
+ if (!this.started || generation !== this.generation) return
95
+ this.touched = dedupeShapes(touched)
96
+ this.bindShapes()
97
+ this.update({ state: this.storeState(), data })
98
+ })
99
+ .catch((error: unknown) => {
100
+ if (!this.started || generation !== this.generation) return
101
+ this.touched = dedupeShapes(touched)
102
+ this.bindShapes()
103
+ this.update({
104
+ state: 'failed',
105
+ ...(this.current.data === undefined ? {} : { data: this.current.data }),
106
+ error,
107
+ })
108
+ })
109
+ .finally(() => {
110
+ if (this.pending === running) this.pending = null
111
+ })
112
+ this.pending = running
113
+ return running
114
+ }
115
+
116
+ private bindShapes(): void {
117
+ this.subscriptions.forEach((unsubscribe) => unsubscribe())
118
+ this.subscriptions = Object.freeze(
119
+ this.touched.map(({ ast, options }) =>
120
+ this.store.subscribe(ast, this.onStoreChange, options),
121
+ ),
122
+ )
123
+ }
124
+
125
+ private onStoreChange = (): void => {
126
+ const state = this.storeState()
127
+ this.update({
128
+ state,
129
+ ...(this.current.data === undefined ? {} : { data: this.current.data }),
130
+ ...(this.current.error === undefined ? {} : { error: this.current.error }),
131
+ })
132
+ if ((state === 'ready' || state === 'optimistic') && !this.rerunQueued) {
133
+ this.rerunQueued = true
134
+ queueMicrotask(() => {
135
+ this.rerunQueued = false
136
+ if (this.started) void this.run('cached')
137
+ })
138
+ }
139
+ }
140
+
141
+ private storeState(): GraphResourceState {
142
+ const snapshots = this.touched.map(({ ast, options }) => this.store.getSnapshot(ast, options))
143
+ if (snapshots.length === 0) return this.current.state
144
+ return combine(snapshots)
145
+ }
146
+
147
+ private update(next: DefinitionSnapshot<Output>): void {
148
+ if (
149
+ next.state === this.current.state &&
150
+ next.data === this.current.data &&
151
+ next.error === this.current.error
152
+ )
153
+ return
154
+ this.current = Object.freeze(next)
155
+ this.listeners.forEach((listener) => listener())
156
+ }
157
+ }
158
+
159
+ /** Execute one named SDK Query definition exclusively through the current GraphStore. */
160
+ export function useQuery<Input, Output, Query extends QueryAST>(
161
+ definition: QueryDefinition<Input, Output, Query> | null,
162
+ input: Input,
163
+ options: QueryResourceOptions = {},
164
+ ): GraphResource<Output> {
165
+ const store = useGraphStore()
166
+ const binding = useDynamicDomain()
167
+ const inputIdentity = stableIdentity(input)
168
+ const observer = useMemo(
169
+ () => (definition === null ? null : new DefinitionObserver(store, binding, definition, input)),
170
+ [store, binding, definition, inputIdentity],
171
+ )
172
+ const idle = useMemo<DefinitionSnapshot<Output>>(() => Object.freeze({ state: 'idle' }), [])
173
+ const subscribe = useCallback(
174
+ (listener: () => void) => observer?.subscribe(listener) ?? (() => {}),
175
+ [observer],
176
+ )
177
+ const snapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle])
178
+ const current = useSyncExternalStore(subscribe, snapshot, snapshot)
179
+ useEffect(() => observer?.start(), [observer])
180
+
181
+ const identity = definition === null ? 'idle' : `${definition.id}\u0000${inputIdentity}`
182
+ useEffect(() => {
183
+ if (observer === null || options.refreshInterval === undefined) return
184
+ return retainRefreshScheduler(
185
+ store,
186
+ identity,
187
+ observer.refresh,
188
+ options.refreshInterval,
189
+ options.refreshWhen ?? 'visible',
190
+ )
191
+ }, [store, observer, identity, options.refreshInterval, options.refreshWhen])
192
+
193
+ const previousRef = useRef<{ readonly identity: string; readonly data: Output } | undefined>(
194
+ undefined,
195
+ )
196
+ const previous =
197
+ options.keepPrevious && previousRef.current?.identity !== identity
198
+ ? previousRef.current
199
+ : undefined
200
+ useEffect(() => {
201
+ if (current.data !== undefined) {
202
+ previousRef.current = Object.freeze({ identity, data: current.data })
203
+ }
204
+ }, [identity, current.data])
205
+
206
+ return useMemo(
207
+ () =>
208
+ Object.freeze({
209
+ state: current.state,
210
+ data: current.data,
211
+ error: current.error,
212
+ ...(previous === undefined ? {} : { previous }),
213
+ transitioning: current.state === 'loading' || current.state === 'refreshing',
214
+ live: false as const,
215
+ sequence: 0 as const,
216
+ refresh: observer?.refresh ?? (() => Promise.resolve()),
217
+ more: Object.freeze({ has: false, pending: false, load: () => Promise.resolve() }),
218
+ }),
219
+ [current, previous, observer],
220
+ )
221
+ }
222
+
223
+ function dedupeShapes(input: readonly TouchedShape[]): readonly TouchedShape[] {
224
+ const seen = new Set<string>()
225
+ return Object.freeze(
226
+ input.filter(({ ast, options }) => {
227
+ const key = `${JSON.stringify(ast)}\u0000${JSON.stringify(options.page)}`
228
+ if (seen.has(key)) return false
229
+ seen.add(key)
230
+ return true
231
+ }),
232
+ )
233
+ }
234
+
235
+ function combine(snapshots: readonly StoreSnapshot[]): GraphResourceState {
236
+ if (snapshots.some((snapshot) => snapshot.state === 'cold')) return 'loading'
237
+ for (const state of ['failed', 'loading', 'refreshing', 'optimistic', 'stale'] as const) {
238
+ if (snapshots.some((snapshot) => snapshot.state === state)) return state
239
+ }
240
+ return 'ready'
241
+ }
242
+
243
+ function stableIdentity(value: unknown): string {
244
+ const seen = new WeakSet<object>()
245
+ const encode = (input: unknown): unknown => {
246
+ if (input === null) return ['null']
247
+ if (typeof input !== 'object') {
248
+ if (typeof input === 'function' || typeof input === 'symbol' || typeof input === 'bigint') {
249
+ throw new TypeError('Query input must be portable and serializable.')
250
+ }
251
+ if (input === undefined) return ['undefined']
252
+ if (typeof input === 'number' && !Number.isFinite(input)) {
253
+ throw new TypeError('Query input numbers must be finite.')
254
+ }
255
+ return [typeof input, input]
256
+ }
257
+ if (seen.has(input)) throw new TypeError('Query input cannot contain cycles.')
258
+ seen.add(input)
259
+ if (Array.isArray(input)) return ['array', input.map(encode)]
260
+ return [
261
+ 'object',
262
+ Object.fromEntries(
263
+ Object.keys(input as Record<string, unknown>)
264
+ .sort()
265
+ .map((key) => [key, encode((input as Record<string, unknown>)[key])]),
266
+ ),
267
+ ]
268
+ }
269
+ return JSON.stringify(encode(value))
270
+ }
@@ -0,0 +1,358 @@
1
+ import type { GraphStore, StoreQueryResponse, StoreSnapshot } from '@astrale-os/kernel-client/store'
2
+ import type { QueryAST } from '@astrale-os/sdk/query'
3
+
4
+ import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'
5
+
6
+ import { useGraphStore } from './store.js'
7
+
8
+ export type GraphResourceState =
9
+ | 'idle'
10
+ | 'loading'
11
+ | 'ready'
12
+ | 'stale'
13
+ | 'refreshing'
14
+ | 'optimistic'
15
+ | 'failed'
16
+
17
+ export interface GraphResource<T> {
18
+ readonly state: GraphResourceState
19
+ /** Current-scope data only. Previous-scope data is never substituted here. */
20
+ readonly data: T | undefined
21
+ readonly error: unknown
22
+ readonly previous?: { readonly identity: string; readonly data: T }
23
+ readonly transitioning: boolean
24
+ readonly live: false
25
+ readonly sequence: 0
26
+ refresh(): Promise<void>
27
+ readonly more: {
28
+ readonly has: boolean
29
+ readonly pending: boolean
30
+ load(): Promise<void>
31
+ }
32
+ }
33
+
34
+ export interface StoreResourceOptions<Output> {
35
+ readonly identity?: string
36
+ readonly keepPrevious?: boolean
37
+ readonly window?: 'page' | 'append'
38
+ readonly merge?: (pages: readonly Output[]) => Output
39
+ readonly refreshInterval?: number
40
+ readonly refreshWhen?: 'always' | 'visible'
41
+ }
42
+
43
+ interface PageRef {
44
+ readonly after?: string
45
+ }
46
+
47
+ interface ObserverSnapshot<Query extends QueryAST> {
48
+ readonly snapshots: readonly StoreSnapshot<Query>[]
49
+ readonly pendingMore: boolean
50
+ }
51
+
52
+ class StoreObserver<Query extends QueryAST> {
53
+ private readonly listeners = new Set<() => void>()
54
+ private pages: readonly PageRef[] = Object.freeze([Object.freeze({})])
55
+ private unsubs: readonly (() => void)[] = Object.freeze([])
56
+ private current: ObserverSnapshot<Query> | undefined
57
+ private morePromise: Promise<void> | null = null
58
+ private started = false
59
+
60
+ constructor(
61
+ private readonly store: GraphStore,
62
+ private readonly query: Query,
63
+ private readonly size: number,
64
+ ) {}
65
+
66
+ subscribe = (listener: () => void): (() => void) => {
67
+ this.listeners.add(listener)
68
+ return () => this.listeners.delete(listener)
69
+ }
70
+
71
+ getSnapshot = (): ObserverSnapshot<Query> => {
72
+ if (this.current !== undefined) return this.current
73
+ this.current = Object.freeze({
74
+ snapshots: Object.freeze(
75
+ this.pages.map((page) => this.store.getSnapshot(this.query, this.pageOptions(page))),
76
+ ),
77
+ pendingMore: this.morePromise !== null,
78
+ })
79
+ return this.current
80
+ }
81
+
82
+ start(): () => void {
83
+ if (!this.started) {
84
+ this.started = true
85
+ this.bindPages()
86
+ void this.store
87
+ .read(this.query, {
88
+ ...this.pageOptions(this.pages[0]!),
89
+ freshness: 'cached',
90
+ })
91
+ .catch(() => {})
92
+ }
93
+ return () => {
94
+ this.started = false
95
+ this.unsubs.forEach((unsubscribe) => unsubscribe())
96
+ this.unsubs = Object.freeze([])
97
+ }
98
+ }
99
+
100
+ async refresh(): Promise<void> {
101
+ await Promise.all(
102
+ this.pages.map((page) =>
103
+ this.store.read(this.query, { ...this.pageOptions(page), freshness: 'refresh' }),
104
+ ),
105
+ )
106
+ }
107
+
108
+ hasMore(): boolean {
109
+ const snapshots = this.getSnapshot().snapshots
110
+ const last = snapshots[snapshots.length - 1]
111
+ const response = last === undefined ? undefined : responseOf(last)
112
+ return response?.page.next !== undefined
113
+ }
114
+
115
+ loadMore(): Promise<void> {
116
+ if (this.morePromise !== null) return this.morePromise
117
+ const snapshots = this.getSnapshot().snapshots
118
+ const last = snapshots[snapshots.length - 1]
119
+ const after = last === undefined ? undefined : responseOf(last)?.page.next
120
+ if (after === undefined) return Promise.resolve()
121
+ if (this.pages.some((page) => page.after === after)) return Promise.resolve()
122
+
123
+ this.pages = Object.freeze([...this.pages, Object.freeze({ after })])
124
+ this.bindPages()
125
+ this.morePromise = this.store
126
+ .read(this.query, { ...this.pageOptions({ after }), freshness: 'cached' })
127
+ .then(() => {})
128
+ .finally(() => {
129
+ this.morePromise = null
130
+ this.publish()
131
+ })
132
+ this.publish()
133
+ return this.morePromise
134
+ }
135
+
136
+ private pageOptions(page: PageRef) {
137
+ return Object.freeze({
138
+ page: Object.freeze({
139
+ size: this.size,
140
+ ...(page.after === undefined ? {} : { after: page.after }),
141
+ }),
142
+ })
143
+ }
144
+
145
+ private bindPages(): void {
146
+ this.unsubs.forEach((unsubscribe) => unsubscribe())
147
+ this.unsubs = Object.freeze(
148
+ this.pages.map((page) =>
149
+ this.store.subscribe(this.query, this.publish, this.pageOptions(page)),
150
+ ),
151
+ )
152
+ this.publish()
153
+ }
154
+
155
+ private publish = (): void => {
156
+ this.current = undefined
157
+ this.listeners.forEach((listener) => listener())
158
+ }
159
+ }
160
+
161
+ interface PollSubscriber {
162
+ readonly interval: number
163
+ readonly when: 'always' | 'visible'
164
+ readonly refresh: () => Promise<void>
165
+ }
166
+
167
+ interface PollScheduler {
168
+ readonly subscribers: Map<symbol, PollSubscriber>
169
+ timer: ReturnType<typeof setTimeout> | undefined
170
+ onVisibility?: () => void
171
+ }
172
+
173
+ const pollers = new WeakMap<GraphStore, Map<string, PollScheduler>>()
174
+
175
+ /** Adapt exact Store pages into a tear-free React resource without a parallel entity cache. */
176
+ export function useStoreResource<Query extends QueryAST, Output>(
177
+ query: Query | null,
178
+ pageSize: number,
179
+ project: (response: StoreQueryResponse<Query>) => Output,
180
+ options: StoreResourceOptions<Output> = {},
181
+ ): GraphResource<Output> {
182
+ const store = useGraphStore()
183
+ const observer = useMemo(
184
+ () => (query === null ? null : new StoreObserver(store, query, positivePage(pageSize))),
185
+ [store, query, pageSize],
186
+ )
187
+ const subscribe = useCallback(
188
+ (listener: () => void) => (observer === null ? () => {} : observer.subscribe(listener)),
189
+ [observer],
190
+ )
191
+ const idle = useMemo<ObserverSnapshot<Query>>(
192
+ () => Object.freeze({ snapshots: Object.freeze([]), pendingMore: false }),
193
+ [],
194
+ )
195
+ const getSnapshot = useCallback(() => observer?.getSnapshot() ?? idle, [observer, idle])
196
+ const current = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
197
+
198
+ useEffect(() => observer?.start(), [observer])
199
+
200
+ const pollIdentity =
201
+ query === null ? null : `${options.identity ?? JSON.stringify(query)}\u0000${pageSize}`
202
+ useEffect(() => {
203
+ if (query === null || observer === null || options.refreshInterval === undefined) return
204
+ return retainRefreshScheduler(
205
+ store,
206
+ pollIdentity!,
207
+ () => observer.refresh(),
208
+ options.refreshInterval,
209
+ options.refreshWhen ?? 'visible',
210
+ )
211
+ }, [store, query, observer, pollIdentity, options.refreshInterval, options.refreshWhen])
212
+
213
+ const refresh = useCallback(async (): Promise<void> => observer?.refresh(), [observer])
214
+ const loadMore = useCallback(async (): Promise<void> => observer?.loadMore(), [observer])
215
+ const identity = options.identity ?? pollIdentity ?? 'idle'
216
+ const projected = useMemo(() => {
217
+ const responses: StoreQueryResponse<Query>[] = current.snapshots.flatMap((snapshot) => {
218
+ const response = responseOf(snapshot)
219
+ return response === undefined ? [] : [response]
220
+ })
221
+ if (responses.length === 0) return undefined
222
+ const pages = responses.map(project)
223
+ if ((options.window ?? 'page') === 'page') return pages[0]
224
+ return options.merge?.(pages) ?? mergeAppendPages(pages)
225
+ }, [current, project, options.window, options.merge])
226
+ const previousRef = useRef<{ readonly identity: string; readonly data: Output } | undefined>(
227
+ undefined,
228
+ )
229
+ const prior = previousRef.current
230
+ const previous =
231
+ options.keepPrevious && prior !== undefined && prior.identity !== identity ? prior : undefined
232
+ useEffect(() => {
233
+ if (projected !== undefined) previousRef.current = Object.freeze({ identity, data: projected })
234
+ }, [identity, projected])
235
+
236
+ return useMemo(() => {
237
+ const snapshots = current.snapshots
238
+ const state = query === null ? 'idle' : combinedState(snapshots)
239
+ const failed = snapshots.find((snapshot) => snapshot.state === 'failed')
240
+ return Object.freeze({
241
+ state,
242
+ data: projected,
243
+ ...(failed?.state === 'failed' ? { error: failed.error } : { error: undefined }),
244
+ ...(previous === undefined ? {} : { previous }),
245
+ transitioning: state === 'loading' || state === 'refreshing',
246
+ live: false as const,
247
+ sequence: 0 as const,
248
+ refresh,
249
+ more: Object.freeze({
250
+ has: observer?.hasMore() ?? false,
251
+ pending: current.pendingMore,
252
+ load: loadMore,
253
+ }),
254
+ })
255
+ }, [current, query, projected, previous, refresh, observer, loadMore])
256
+ }
257
+
258
+ function combinedState(snapshots: readonly StoreSnapshot[]): GraphResourceState {
259
+ if (snapshots.length === 0 || snapshots.some((snapshot) => snapshot.state === 'cold'))
260
+ return 'loading'
261
+ for (const state of ['failed', 'loading', 'refreshing', 'optimistic', 'stale'] as const) {
262
+ if (snapshots.some((snapshot) => snapshot.state === state)) return state
263
+ }
264
+ return 'ready'
265
+ }
266
+
267
+ function responseOf<Query extends QueryAST>(
268
+ snapshot: StoreSnapshot<Query>,
269
+ ): StoreQueryResponse<Query> | undefined {
270
+ return 'response' in snapshot ? snapshot.response : undefined
271
+ }
272
+
273
+ function mergeAppendPages<Output>(pages: readonly Output[]): Output {
274
+ const first = pages[0]
275
+ if (!Array.isArray(first)) return pages[pages.length - 1] as Output
276
+ const seen = new Set<unknown>()
277
+ const merged: unknown[] = []
278
+ for (const page of pages as readonly (readonly unknown[])[]) {
279
+ for (const value of page) {
280
+ const identity =
281
+ value !== null && typeof value === 'object' && 'id' in value
282
+ ? (value as { readonly id: unknown }).id
283
+ : value
284
+ if (seen.has(identity)) continue
285
+ seen.add(identity)
286
+ merged.push(value)
287
+ }
288
+ }
289
+ return Object.freeze(merged) as Output
290
+ }
291
+
292
+ export function retainRefreshScheduler(
293
+ store: GraphStore,
294
+ identity: string,
295
+ refresh: () => Promise<void>,
296
+ interval: number,
297
+ when: 'always' | 'visible',
298
+ ): () => void {
299
+ if (!Number.isSafeInteger(interval) || interval < 250) {
300
+ throw new TypeError('refreshInterval must be an integer of at least 250ms.')
301
+ }
302
+ let byIdentity = pollers.get(store)
303
+ if (byIdentity === undefined) {
304
+ byIdentity = new Map()
305
+ pollers.set(store, byIdentity)
306
+ }
307
+ let scheduler = byIdentity.get(identity)
308
+ if (scheduler === undefined) {
309
+ scheduler = { subscribers: new Map(), timer: undefined }
310
+ byIdentity.set(identity, scheduler)
311
+ }
312
+ const token = Symbol(identity)
313
+ scheduler.subscribers.set(token, { interval, when, refresh })
314
+ schedulePoll(scheduler)
315
+ if (typeof document !== 'undefined' && scheduler.onVisibility === undefined) {
316
+ scheduler.onVisibility = () => schedulePoll(scheduler!, true)
317
+ document.addEventListener('visibilitychange', scheduler.onVisibility)
318
+ }
319
+ return () => {
320
+ scheduler!.subscribers.delete(token)
321
+ if (scheduler!.subscribers.size !== 0) {
322
+ schedulePoll(scheduler!)
323
+ return
324
+ }
325
+ if (scheduler!.timer !== undefined) clearTimeout(scheduler!.timer)
326
+ if (typeof document !== 'undefined' && scheduler!.onVisibility !== undefined) {
327
+ document.removeEventListener('visibilitychange', scheduler!.onVisibility)
328
+ }
329
+ byIdentity!.delete(identity)
330
+ if (byIdentity!.size === 0) pollers.delete(store)
331
+ }
332
+ }
333
+
334
+ function schedulePoll(scheduler: PollScheduler, immediate = false): void {
335
+ if (scheduler.timer !== undefined) clearTimeout(scheduler.timer)
336
+ const subscribers = [...scheduler.subscribers.values()]
337
+ if (subscribers.length === 0) return
338
+ const interval = Math.min(...subscribers.map((subscriber) => subscriber.interval))
339
+ scheduler.timer = setTimeout(
340
+ () => {
341
+ scheduler.timer = undefined
342
+ const visible = typeof document === 'undefined' || document.visibilityState !== 'hidden'
343
+ const eligible = [...scheduler.subscribers.values()].filter(
344
+ (subscriber) => subscriber.when === 'always' || visible,
345
+ )
346
+ eligible.forEach((subscriber) => void subscriber.refresh().catch(() => {}))
347
+ schedulePoll(scheduler)
348
+ },
349
+ immediate ? 0 : interval,
350
+ )
351
+ }
352
+
353
+ function positivePage(input: number): number {
354
+ if (!Number.isSafeInteger(input) || input < 1) {
355
+ throw new TypeError('Graph resource page size must be a positive safe integer.')
356
+ }
357
+ return input
358
+ }