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

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 (335) 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/astrale.d.ts +14 -0
  39. package/dist/components/astrale.d.ts.map +1 -0
  40. package/dist/components/astrale.js +12 -0
  41. package/dist/components/boundaries.d.ts +47 -0
  42. package/dist/components/boundaries.d.ts.map +1 -0
  43. package/dist/components/boundaries.js +55 -0
  44. package/dist/components/can.d.ts +11 -0
  45. package/dist/components/can.d.ts.map +1 -0
  46. package/dist/components/can.js +5 -0
  47. package/dist/components/index.d.ts +6 -0
  48. package/dist/components/index.d.ts.map +1 -0
  49. package/dist/components/index.js +3 -0
  50. package/dist/graph/call/call.hook.d.ts +8 -18
  51. package/dist/graph/call/call.hook.d.ts.map +1 -1
  52. package/dist/graph/call/call.hook.js +7 -13
  53. package/dist/graph/call/stream.hook.d.ts +12 -20
  54. package/dist/graph/call/stream.hook.d.ts.map +1 -1
  55. package/dist/graph/call/stream.hook.js +55 -111
  56. package/dist/graph/graph.context.d.ts +4 -3
  57. package/dist/graph/graph.context.d.ts.map +1 -1
  58. package/dist/graph/graph.context.js +2 -8
  59. package/dist/graph/guards.error.d.ts +7 -8
  60. package/dist/graph/guards.error.d.ts.map +1 -1
  61. package/dist/graph/guards.error.js +11 -7
  62. package/dist/graph/index.d.ts +8 -16
  63. package/dist/graph/index.d.ts.map +1 -1
  64. package/dist/graph/index.js +3 -7
  65. package/dist/graph/memory/read/envelope.d.ts +22 -54
  66. package/dist/graph/memory/read/envelope.d.ts.map +1 -1
  67. package/dist/graph/memory/read/envelope.js +26 -20
  68. package/dist/graph/memory/read/idle.d.ts +1 -15
  69. package/dist/graph/memory/read/idle.d.ts.map +1 -1
  70. package/dist/graph/memory/read/idle.js +4 -13
  71. package/dist/graph/query-definition.hook.d.ts +11 -0
  72. package/dist/graph/query-definition.hook.d.ts.map +1 -0
  73. package/dist/graph/query-definition.hook.js +205 -0
  74. package/dist/graph/resource.d.ts +34 -0
  75. package/dist/graph/resource.d.ts.map +1 -0
  76. package/dist/graph/resource.js +244 -0
  77. package/dist/graph/store.d.ts +25 -0
  78. package/dist/graph/store.d.ts.map +1 -0
  79. package/dist/graph/store.js +69 -0
  80. package/dist/host/actions.hook.d.ts +34 -0
  81. package/dist/host/actions.hook.d.ts.map +1 -0
  82. package/dist/host/actions.hook.js +125 -0
  83. package/dist/host/components.d.ts +15 -0
  84. package/dist/host/components.d.ts.map +1 -0
  85. package/dist/host/components.js +14 -0
  86. package/dist/host/index.d.ts +5 -0
  87. package/dist/host/index.d.ts.map +1 -0
  88. package/dist/host/index.js +3 -0
  89. package/dist/index.d.ts +26 -37
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +20 -31
  92. package/dist/intents/envelope.d.ts +7 -19
  93. package/dist/intents/envelope.d.ts.map +1 -1
  94. package/dist/intents/envelope.js +2 -3
  95. package/dist/intents/send-intent.hook.d.ts +5 -7
  96. package/dist/intents/send-intent.hook.d.ts.map +1 -1
  97. package/dist/intents/send-intent.hook.js +17 -75
  98. package/dist/navigation/adapters/adapter.d.ts +5 -0
  99. package/dist/navigation/adapters/adapter.d.ts.map +1 -1
  100. package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -1
  101. package/dist/navigation/adapters/memory.adapter.js +4 -0
  102. package/dist/navigation/adapters/url.adapter.d.ts.map +1 -1
  103. package/dist/navigation/adapters/url.adapter.js +57 -10
  104. package/dist/navigation/codec.d.ts +60 -0
  105. package/dist/navigation/codec.d.ts.map +1 -0
  106. package/dist/navigation/codec.js +121 -0
  107. package/dist/navigation/history.d.ts +11 -9
  108. package/dist/navigation/history.d.ts.map +1 -1
  109. package/dist/navigation/history.js +6 -8
  110. package/dist/navigation/hooks/ancestors.hook.d.ts +5 -7
  111. package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -1
  112. package/dist/navigation/hooks/ancestors.hook.js +7 -51
  113. package/dist/navigation/hooks/location.hook.d.ts.map +1 -1
  114. package/dist/navigation/hooks/location.hook.js +4 -6
  115. package/dist/navigation/hooks/scope.hook.d.ts.map +1 -1
  116. package/dist/navigation/hooks/scope.hook.js +4 -6
  117. package/dist/navigation/index.d.ts +4 -2
  118. package/dist/navigation/index.d.ts.map +1 -1
  119. package/dist/navigation/index.js +2 -1
  120. package/dist/navigation/link.d.ts +12 -0
  121. package/dist/navigation/link.d.ts.map +1 -1
  122. package/dist/navigation/link.js +13 -2
  123. package/dist/navigation/location.d.ts +13 -40
  124. package/dist/navigation/location.d.ts.map +1 -1
  125. package/dist/navigation/location.js +14 -76
  126. package/dist/navigation/scope.context.d.ts +17 -26
  127. package/dist/navigation/scope.context.d.ts.map +1 -1
  128. package/dist/navigation/scope.context.js +140 -108
  129. package/dist/navigation/snapshot.d.ts +14 -19
  130. package/dist/navigation/snapshot.d.ts.map +1 -1
  131. package/dist/navigation/snapshot.js +48 -44
  132. package/dist/schema/action.hook.d.ts +44 -0
  133. package/dist/schema/action.hook.d.ts.map +1 -0
  134. package/dist/schema/action.hook.js +72 -0
  135. package/dist/schema/domain.hook.d.ts +12 -7
  136. package/dist/schema/domain.hook.d.ts.map +1 -1
  137. package/dist/schema/domain.hook.js +101 -4
  138. package/dist/schema/index.d.ts +9 -8
  139. package/dist/schema/index.d.ts.map +1 -1
  140. package/dist/schema/index.js +5 -7
  141. package/dist/schema/kernel.hook.d.ts +4 -9
  142. package/dist/schema/kernel.hook.d.ts.map +1 -1
  143. package/dist/schema/kernel.hook.js +4 -6
  144. package/dist/schema/mutation.hook.d.ts +9 -0
  145. package/dist/schema/mutation.hook.d.ts.map +1 -0
  146. package/dist/schema/mutation.hook.js +41 -0
  147. package/dist/schema/object.hook.d.ts +16 -0
  148. package/dist/schema/object.hook.d.ts.map +1 -0
  149. package/dist/schema/object.hook.js +46 -0
  150. package/dist/schema/relation.hook.d.ts +46 -0
  151. package/dist/schema/relation.hook.d.ts.map +1 -0
  152. package/dist/schema/relation.hook.js +136 -0
  153. package/dist/selection/live.d.ts +9 -17
  154. package/dist/selection/live.d.ts.map +1 -1
  155. package/dist/selection/live.js +65 -112
  156. package/dist/selection/scope.context.js +5 -4
  157. package/dist/session/boot/boot.d.ts +1 -13
  158. package/dist/session/boot/boot.d.ts.map +1 -1
  159. package/dist/session/boot/boot.js +9 -27
  160. package/dist/session/index.d.ts +1 -0
  161. package/dist/session/index.d.ts.map +1 -1
  162. package/dist/session/kernel-instance.hook.d.ts +1 -4
  163. package/dist/session/kernel-instance.hook.d.ts.map +1 -1
  164. package/dist/session/kernel-instance.hook.js +1 -5
  165. package/dist/session/provider.d.ts +9 -23
  166. package/dist/session/provider.d.ts.map +1 -1
  167. package/dist/session/provider.js +29 -75
  168. package/dist/session/session.context.d.ts +5 -0
  169. package/dist/session/session.context.d.ts.map +1 -1
  170. package/dist/window/adopt.d.ts +0 -1
  171. package/dist/window/adopt.d.ts.map +1 -1
  172. package/dist/window/hooks/target-node.hook.d.ts +10 -6
  173. package/dist/window/hooks/target-node.hook.d.ts.map +1 -1
  174. package/dist/window/hooks/target-node.hook.js +14 -72
  175. package/dist/window/hooks/views-for.hook.d.ts +4 -3
  176. package/dist/window/hooks/views-for.hook.d.ts.map +1 -1
  177. package/dist/window/hooks/views-for.hook.js +6 -22
  178. package/dist/window/hooks/windows.hook.d.ts.map +1 -1
  179. package/dist/window/hooks/windows.hook.js +3 -4
  180. package/dist/window/host.d.ts +9 -11
  181. package/dist/window/host.d.ts.map +1 -1
  182. package/dist/window/host.js +26 -62
  183. package/dist/window/hub.d.ts +2 -0
  184. package/dist/window/hub.d.ts.map +1 -1
  185. package/dist/window/hub.js +33 -0
  186. package/dist/window/index.d.ts +2 -2
  187. package/dist/window/index.d.ts.map +1 -1
  188. package/dist/window/index.js +2 -2
  189. package/dist/window/lifecycle/mount.d.ts +5 -44
  190. package/dist/window/lifecycle/mount.d.ts.map +1 -1
  191. package/dist/window/lifecycle/mount.js +14 -120
  192. package/dist/window/lifecycle/states.d.ts +0 -5
  193. package/dist/window/lifecycle/states.d.ts.map +1 -1
  194. package/package.json +20 -8
  195. package/src/application/apps/app-host.tsx +11 -15
  196. package/src/application/apps/apps.hook.ts +13 -6
  197. package/src/application/apps/domains.hook.ts +1 -1
  198. package/src/application/apps/open-app.hook.ts +3 -4
  199. package/src/application/index.ts +4 -6
  200. package/src/application/spaces/space.hook.ts +5 -6
  201. package/src/application/spaces/spaces.hook.ts +14 -9
  202. package/src/auth/act-as.context.tsx +37 -91
  203. package/src/auth/auth.hook.ts +2 -30
  204. package/src/auth/can.hook.ts +44 -135
  205. package/src/auth/index.ts +1 -0
  206. package/src/auth/self.hook.ts +2 -9
  207. package/src/auth/session-kernel.ts +6 -16
  208. package/src/components/astrale.tsx +40 -0
  209. package/src/components/boundaries.tsx +113 -0
  210. package/src/components/can.tsx +16 -0
  211. package/src/components/index.ts +13 -0
  212. package/src/graph/call/call.hook.ts +22 -27
  213. package/src/graph/call/stream.hook.ts +66 -143
  214. package/src/graph/graph.context.ts +6 -11
  215. package/src/graph/guards.error.ts +17 -55
  216. package/src/graph/index.ts +23 -17
  217. package/src/graph/memory/read/envelope.ts +40 -65
  218. package/src/graph/memory/read/idle.ts +5 -18
  219. package/src/graph/query-definition.hook.ts +270 -0
  220. package/src/graph/resource.ts +358 -0
  221. package/src/graph/store.ts +97 -0
  222. package/src/host/actions.hook.ts +165 -0
  223. package/src/host/components.tsx +29 -0
  224. package/src/host/index.ts +17 -0
  225. package/src/index.ts +131 -124
  226. package/src/intents/envelope.ts +7 -23
  227. package/src/intents/send-intent.hook.ts +24 -79
  228. package/src/navigation/adapters/adapter.ts +3 -0
  229. package/src/navigation/adapters/memory.adapter.ts +4 -0
  230. package/src/navigation/adapters/url.adapter.ts +48 -8
  231. package/src/navigation/codec.ts +190 -0
  232. package/src/navigation/history.ts +20 -11
  233. package/src/navigation/hooks/ancestors.hook.ts +10 -60
  234. package/src/navigation/hooks/location.hook.ts +4 -6
  235. package/src/navigation/hooks/scope.hook.ts +4 -6
  236. package/src/navigation/index.ts +18 -2
  237. package/src/navigation/link.tsx +27 -2
  238. package/src/navigation/location.ts +26 -93
  239. package/src/navigation/scope.context.tsx +151 -136
  240. package/src/navigation/snapshot.ts +69 -50
  241. package/src/schema/action.hook.ts +146 -0
  242. package/src/schema/domain.hook.ts +127 -11
  243. package/src/schema/index.ts +9 -30
  244. package/src/schema/kernel.hook.ts +6 -13
  245. package/src/schema/mutation.hook.ts +63 -0
  246. package/src/schema/object.hook.ts +85 -0
  247. package/src/schema/relation.hook.ts +253 -0
  248. package/src/selection/live.ts +75 -125
  249. package/src/selection/scope.context.tsx +4 -4
  250. package/src/session/boot/boot.ts +8 -33
  251. package/src/session/index.ts +1 -0
  252. package/src/session/kernel-instance.hook.ts +2 -11
  253. package/src/session/provider.tsx +34 -103
  254. package/src/session/session.context.ts +6 -0
  255. package/src/window/adopt.ts +0 -1
  256. package/src/window/hooks/target-node.hook.ts +20 -85
  257. package/src/window/hooks/views-for.hook.ts +12 -25
  258. package/src/window/hooks/windows.hook.ts +3 -4
  259. package/src/window/host.tsx +35 -77
  260. package/src/window/hub.ts +33 -0
  261. package/src/window/index.ts +3 -3
  262. package/src/window/lifecycle/mount.ts +15 -160
  263. package/src/window/lifecycle/states.ts +1 -5
  264. package/dist/application/roles.hook.d.ts +0 -7
  265. package/dist/application/roles.hook.d.ts.map +0 -1
  266. package/dist/application/roles.hook.js +0 -23
  267. package/dist/graph/memory/base.d.ts +0 -21
  268. package/dist/graph/memory/base.d.ts.map +0 -1
  269. package/dist/graph/memory/base.js +0 -33
  270. package/dist/graph/memory/read/read.d.ts +0 -9
  271. package/dist/graph/memory/read/read.d.ts.map +0 -1
  272. package/dist/graph/memory/read/read.js +0 -87
  273. package/dist/graph/memory/scope.d.ts +0 -7
  274. package/dist/graph/memory/scope.d.ts.map +0 -1
  275. package/dist/graph/memory/scope.js +0 -25
  276. package/dist/graph/mutate/move.hook.d.ts +0 -7
  277. package/dist/graph/mutate/move.hook.d.ts.map +0 -1
  278. package/dist/graph/mutate/move.hook.js +0 -49
  279. package/dist/graph/mutate/mutate.hook.d.ts +0 -17
  280. package/dist/graph/mutate/mutate.hook.d.ts.map +0 -1
  281. package/dist/graph/mutate/mutate.hook.js +0 -45
  282. package/dist/graph/query/children.hook.d.ts +0 -29
  283. package/dist/graph/query/children.hook.d.ts.map +0 -1
  284. package/dist/graph/query/children.hook.js +0 -64
  285. package/dist/graph/query/graph.hook.d.ts +0 -9
  286. package/dist/graph/query/graph.hook.d.ts.map +0 -1
  287. package/dist/graph/query/graph.hook.js +0 -5
  288. package/dist/graph/query/neighbors.hook.d.ts +0 -7
  289. package/dist/graph/query/neighbors.hook.d.ts.map +0 -1
  290. package/dist/graph/query/neighbors.hook.js +0 -104
  291. package/dist/graph/query/node.hook.d.ts +0 -14
  292. package/dist/graph/query/node.hook.d.ts.map +0 -1
  293. package/dist/graph/query/node.hook.js +0 -43
  294. package/dist/graph/query/query.hook.d.ts +0 -14
  295. package/dist/graph/query/query.hook.d.ts.map +0 -1
  296. package/dist/graph/query/query.hook.js +0 -23
  297. package/dist/schema/bind-read.d.ts +0 -37
  298. package/dist/schema/bind-read.d.ts.map +0 -1
  299. package/dist/schema/bind-read.js +0 -205
  300. package/dist/schema/binding.d.ts +0 -21
  301. package/dist/schema/binding.d.ts.map +0 -1
  302. package/dist/schema/binding.js +0 -99
  303. package/dist/schema/function.hook.d.ts +0 -9
  304. package/dist/schema/function.hook.d.ts.map +0 -1
  305. package/dist/schema/function.hook.js +0 -40
  306. package/dist/schema/method.hook.d.ts +0 -14
  307. package/dist/schema/method.hook.d.ts.map +0 -1
  308. package/dist/schema/method.hook.js +0 -49
  309. package/dist/schema/tag.d.ts +0 -11
  310. package/dist/schema/tag.d.ts.map +0 -1
  311. package/dist/schema/tag.js +0 -11
  312. package/dist/schema/types.d.ts +0 -73
  313. package/dist/schema/types.d.ts.map +0 -1
  314. package/dist/schema/types.js +0 -1
  315. package/dist/schema/write.d.ts +0 -16
  316. package/dist/schema/write.d.ts.map +0 -1
  317. package/dist/schema/write.js +0 -56
  318. package/src/application/roles.hook.ts +0 -41
  319. package/src/graph/memory/base.ts +0 -50
  320. package/src/graph/memory/read/read.ts +0 -103
  321. package/src/graph/memory/scope.ts +0 -29
  322. package/src/graph/mutate/move.hook.ts +0 -70
  323. package/src/graph/mutate/mutate.hook.ts +0 -69
  324. package/src/graph/query/children.hook.ts +0 -136
  325. package/src/graph/query/graph.hook.ts +0 -13
  326. package/src/graph/query/neighbors.hook.ts +0 -159
  327. package/src/graph/query/node.hook.ts +0 -83
  328. package/src/graph/query/query.hook.ts +0 -42
  329. package/src/schema/bind-read.ts +0 -319
  330. package/src/schema/binding.ts +0 -146
  331. package/src/schema/function.hook.ts +0 -68
  332. package/src/schema/method.hook.ts +0 -103
  333. package/src/schema/tag.ts +0 -19
  334. package/src/schema/types.ts +0 -150
  335. package/src/schema/write.ts +0 -96
@@ -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
+ }
@@ -0,0 +1,97 @@
1
+ import type { GraphApi } from '@astrale-os/kernel-client/graph'
2
+ import type { GraphStore as GraphStoreValue } from '@astrale-os/kernel-client/store'
3
+ import type { Shell } from '@astrale-os/shell'
4
+
5
+ import { GraphStore } from '@astrale-os/kernel-client/store'
6
+ import { useContext } from 'react'
7
+
8
+ import { GraphContext } from './graph.context.js'
9
+
10
+ interface StoreEntry {
11
+ readonly shell: Shell
12
+ readonly graph: GraphApi
13
+ readonly scope: string
14
+ readonly store: GraphStoreValue
15
+ readonly owned: boolean
16
+ retained: number
17
+ generation: number
18
+ }
19
+
20
+ const entries = new WeakMap<Shell, StoreEntry>()
21
+
22
+ /** Authority/schema identity. Session ids and Publication ETags are deliberately absent. */
23
+ export interface GraphStoreScope {
24
+ readonly authority: string
25
+ readonly schemaRevision: string
26
+ }
27
+
28
+ type StoreIdentity = Awaited<ReturnType<Shell['kernel']['auth']['whoami']>>
29
+
30
+ export async function resolveGraphStoreScope(
31
+ shell: Shell,
32
+ knownIdentity?: StoreIdentity | null,
33
+ ): Promise<GraphStoreScope> {
34
+ const [identity, snapshot] = await Promise.all([
35
+ knownIdentity === undefined
36
+ ? shell.kernel.auth.whoami().catch(() => null)
37
+ : Promise.resolve(knownIdentity),
38
+ shell.kernel.snapshot(),
39
+ ])
40
+ return Object.freeze({
41
+ authority:
42
+ identity === null
43
+ ? 'anonymous'
44
+ : `${identity.issuer}\u0000${identity.subject}\u0000${identity.id}`,
45
+ schemaRevision: snapshot.publication.schema.revision,
46
+ })
47
+ }
48
+
49
+ export function storeFor(shell: Shell, scope: GraphStoreScope): StoreEntry {
50
+ const current = entries.get(shell)
51
+ const scopeKey = `${scope.authority}\u0000${scope.schemaRevision}`
52
+ if (current !== undefined && current.graph === shell.kernel.graph && current.scope === scopeKey) {
53
+ return current
54
+ }
55
+ const supplied = shell.kernel.store
56
+ const created: StoreEntry = {
57
+ shell,
58
+ graph: shell.kernel.graph,
59
+ scope: scopeKey,
60
+ // A supplied Store is trusted only for the KernelClient's first declared scope. A later
61
+ // authority/revision/GraphApi replacement gets a new Store and cannot inherit old memory.
62
+ store:
63
+ current === undefined && supplied !== undefined
64
+ ? supplied
65
+ : new GraphStore({ graph: shell.kernel.graph }),
66
+ owned: current !== undefined || supplied === undefined,
67
+ retained: 0,
68
+ generation: 0,
69
+ }
70
+ entries.set(shell, created)
71
+ if (current !== undefined && current.retained === 0 && current.owned) current.store.close()
72
+ return created
73
+ }
74
+
75
+ export function retainStore(entry: StoreEntry): () => void {
76
+ entry.generation += 1
77
+ entry.retained += 1
78
+ let released = false
79
+ return () => {
80
+ if (released) return
81
+ released = true
82
+ entry.retained -= 1
83
+ const generation = ++entry.generation
84
+ queueMicrotask(() => {
85
+ if (entry.retained !== 0 || entry.generation !== generation || !entry.owned) return
86
+ if (entries.get(entry.shell) === entry) entries.delete(entry.shell)
87
+ entry.store.close()
88
+ })
89
+ }
90
+ }
91
+
92
+ /** The exact caller/schema-scoped GraphStore for the current View subtree. */
93
+ export function useGraphStore(): GraphStoreValue {
94
+ const store = useContext(GraphContext)
95
+ if (store === null) throw new Error('useGraphStore must be used within a <ShellProvider>')
96
+ return store
97
+ }
@@ -0,0 +1,165 @@
1
+ import type {
2
+ AccessRequest,
3
+ AccessResult,
4
+ ExternalOpenRequest,
5
+ ExternalOpenResult,
6
+ HostCapabilities,
7
+ ModalRequest,
8
+ ModalResult,
9
+ NotificationRequest,
10
+ NotificationResult,
11
+ OpenViewRequest,
12
+ OpenViewResult,
13
+ } from '@astrale-os/shell'
14
+
15
+ import { useMemo } from 'react'
16
+
17
+ import { useSendIntent } from '../intents/send-intent.hook.js'
18
+ import { useShell } from '../session/shell.hook.js'
19
+
20
+ export function useHostCapabilities(): HostCapabilities {
21
+ return useShell().hostCapabilities
22
+ }
23
+
24
+ export class HostCapabilityUnavailableError extends Error {
25
+ readonly code = 'SHELL_REACT_HOST_CAPABILITY_UNAVAILABLE' as const
26
+ constructor(readonly capability: string) {
27
+ super(`Host capability '${capability}' is unavailable.`)
28
+ this.name = 'HostCapabilityUnavailableError'
29
+ }
30
+ }
31
+
32
+ export interface HostDialogApi {
33
+ readonly supported: boolean
34
+ open(request: ModalRequest): Promise<ModalResult>
35
+ confirm(request: Omit<ModalRequest, 'nested'>): Promise<boolean>
36
+ }
37
+
38
+ export function useHostDialog(): HostDialogApi {
39
+ const capabilities = useHostCapabilities()
40
+ const send = useSendIntent()
41
+ return useMemo(
42
+ () => ({
43
+ supported: capabilities.actions.modal !== undefined,
44
+ open: (request: ModalRequest) => {
45
+ if (capabilities.actions.modal === undefined) return unsupported('actions.modal')
46
+ portable(request)
47
+ return send('actions.modal', request)
48
+ },
49
+ confirm: async (request: Omit<ModalRequest, 'nested'>) => {
50
+ if (capabilities.actions.modal === undefined) return unsupported('actions.modal')
51
+ portable(request)
52
+ return (await send('actions.modal', request)).outcome === 'confirmed'
53
+ },
54
+ }),
55
+ [capabilities, send],
56
+ )
57
+ }
58
+
59
+ export interface NotifyApi {
60
+ (request: NotificationRequest): Promise<NotificationResult>
61
+ readonly supported: boolean
62
+ }
63
+
64
+ export function useNotify(): NotifyApi {
65
+ const capabilities = useHostCapabilities()
66
+ const send = useSendIntent()
67
+ return useMemo(() => {
68
+ const notify = ((request: NotificationRequest) => {
69
+ if (capabilities.actions.notify === undefined) return unsupported('actions.notify')
70
+ portable(request)
71
+ return send('actions.notify', request)
72
+ }) as NotifyApi
73
+ Object.defineProperty(notify, 'supported', {
74
+ enumerable: true,
75
+ value: capabilities.actions.notify !== undefined,
76
+ })
77
+ return notify
78
+ }, [capabilities, send])
79
+ }
80
+
81
+ export interface OpenViewApi {
82
+ (request: OpenViewRequest): Promise<OpenViewResult>
83
+ readonly supported: boolean
84
+ }
85
+
86
+ export function useOpenView(): OpenViewApi {
87
+ const capabilities = useHostCapabilities()
88
+ const send = useSendIntent()
89
+ return useMemo(() => {
90
+ const open = ((request: OpenViewRequest) => {
91
+ if (capabilities.navigation.openView === undefined) return unsupported('view.open')
92
+ portable(request)
93
+ return send('view.open', request)
94
+ }) as OpenViewApi
95
+ Object.defineProperty(open, 'supported', {
96
+ enumerable: true,
97
+ value: capabilities.navigation.openView !== undefined,
98
+ })
99
+ return open
100
+ }, [capabilities, send])
101
+ }
102
+
103
+ export interface ExternalOpenApi {
104
+ (request: ExternalOpenRequest): Promise<ExternalOpenResult>
105
+ readonly supported: boolean
106
+ }
107
+
108
+ export function useExternalOpen(): ExternalOpenApi {
109
+ const capabilities = useHostCapabilities()
110
+ const send = useSendIntent()
111
+ return useMemo(() => {
112
+ const open = ((request: ExternalOpenRequest) => {
113
+ if (capabilities.navigation.external === undefined) return unsupported('browser.openExternal')
114
+ portable(request)
115
+ return send('browser.openExternal', request)
116
+ }) as ExternalOpenApi
117
+ Object.defineProperty(open, 'supported', {
118
+ enumerable: true,
119
+ value: capabilities.navigation.external !== undefined,
120
+ })
121
+ return open
122
+ }, [capabilities, send])
123
+ }
124
+
125
+ export interface RequestAccessApi {
126
+ (request: AccessRequest): Promise<AccessResult>
127
+ readonly supported: boolean
128
+ }
129
+
130
+ export function useRequestAccess(): RequestAccessApi {
131
+ const capabilities = useHostCapabilities()
132
+ const send = useSendIntent()
133
+ return useMemo(() => {
134
+ const request = ((input: AccessRequest) => {
135
+ if (capabilities.access.request === undefined) return unsupported('access.request')
136
+ portable(input)
137
+ return send('access.request', input)
138
+ }) as RequestAccessApi
139
+ Object.defineProperty(request, 'supported', {
140
+ enumerable: true,
141
+ value: capabilities.access.request !== undefined,
142
+ })
143
+ return request
144
+ }, [capabilities, send])
145
+ }
146
+
147
+ function unsupported<Result>(capability: string): Promise<Result> {
148
+ return Promise.reject(new HostCapabilityUnavailableError(capability))
149
+ }
150
+
151
+ function portable(input: unknown): void {
152
+ const seen = new WeakSet<object>()
153
+ const visit = (value: unknown): void => {
154
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return
155
+ if (typeof value === 'number' && Number.isFinite(value)) return
156
+ if (typeof value !== 'object' || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
157
+ throw new TypeError('Host action payload must be serializable data.')
158
+ }
159
+ if (seen.has(value)) throw new TypeError('Host action payload cannot contain cycles.')
160
+ seen.add(value)
161
+ if (Array.isArray(value)) value.forEach(visit)
162
+ else Object.values(value).forEach(visit)
163
+ }
164
+ visit(input)
165
+ }
@@ -0,0 +1,29 @@
1
+ import type { ReactElement, ReactNode } from 'react'
2
+
3
+ import type { HostDialogApi, NotifyApi, OpenViewApi, RequestAccessApi } from './actions.hook.js'
4
+
5
+ import { useHostDialog, useNotify, useOpenView, useRequestAccess } from './actions.hook.js'
6
+
7
+ export function HostDialog(props: {
8
+ readonly children: (dialog: HostDialogApi) => ReactNode
9
+ }): ReactElement {
10
+ return <>{props.children(useHostDialog())}</>
11
+ }
12
+
13
+ export function Notify(props: {
14
+ readonly children: (notify: NotifyApi) => ReactNode
15
+ }): ReactElement {
16
+ return <>{props.children(useNotify())}</>
17
+ }
18
+
19
+ export function NestedView(props: {
20
+ readonly children: (open: OpenViewApi) => ReactNode
21
+ }): ReactElement {
22
+ return <>{props.children(useOpenView())}</>
23
+ }
24
+
25
+ export function RequestAccess(props: {
26
+ readonly children: (request: RequestAccessApi) => ReactNode
27
+ }): ReactElement {
28
+ return <>{props.children(useRequestAccess())}</>
29
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ useExternalOpen,
3
+ useHostCapabilities,
4
+ useHostDialog,
5
+ useNotify,
6
+ useOpenView,
7
+ useRequestAccess,
8
+ } from './actions.hook.js'
9
+ export type {
10
+ ExternalOpenApi,
11
+ HostDialogApi,
12
+ NotifyApi,
13
+ OpenViewApi,
14
+ RequestAccessApi,
15
+ } from './actions.hook.js'
16
+ export { HostDialog, NestedView, Notify, RequestAccess } from './components.js'
17
+ export { HostCapabilityUnavailableError } from './actions.hook.js'