@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.
- package/dist/application/apps/app-host.d.ts +4 -15
- package/dist/application/apps/app-host.d.ts.map +1 -1
- package/dist/application/apps/app-host.js +1 -1
- package/dist/application/apps/apps.hook.d.ts +7 -4
- package/dist/application/apps/apps.hook.d.ts.map +1 -1
- package/dist/application/apps/apps.hook.js +10 -2
- package/dist/application/apps/domains.hook.d.ts +1 -1
- package/dist/application/apps/domains.hook.d.ts.map +1 -1
- package/dist/application/apps/domains.hook.js +1 -1
- package/dist/application/apps/open-app.hook.d.ts +2 -3
- package/dist/application/apps/open-app.hook.d.ts.map +1 -1
- package/dist/application/index.d.ts +2 -4
- package/dist/application/index.d.ts.map +1 -1
- package/dist/application/index.js +2 -3
- package/dist/application/spaces/space.hook.d.ts +2 -2
- package/dist/application/spaces/space.hook.d.ts.map +1 -1
- package/dist/application/spaces/space.hook.js +2 -2
- package/dist/application/spaces/spaces.hook.d.ts +2 -3
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -1
- package/dist/application/spaces/spaces.hook.js +10 -3
- package/dist/auth/act-as.context.d.ts +15 -32
- package/dist/auth/act-as.context.d.ts.map +1 -1
- package/dist/auth/act-as.context.js +22 -66
- package/dist/auth/auth.hook.d.ts +1 -0
- package/dist/auth/auth.hook.d.ts.map +1 -1
- package/dist/auth/auth.hook.js +2 -22
- package/dist/auth/can.hook.d.ts +10 -38
- package/dist/auth/can.hook.d.ts.map +1 -1
- package/dist/auth/can.hook.js +35 -110
- package/dist/auth/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/self.hook.d.ts +2 -2
- package/dist/auth/self.hook.d.ts.map +1 -1
- package/dist/auth/self.hook.js +0 -7
- package/dist/auth/session-kernel.d.ts +2 -3
- package/dist/auth/session-kernel.d.ts.map +1 -1
- package/dist/auth/session-kernel.js +4 -13
- package/dist/components/astrale.d.ts +14 -0
- package/dist/components/astrale.d.ts.map +1 -0
- package/dist/components/astrale.js +12 -0
- package/dist/components/boundaries.d.ts +47 -0
- package/dist/components/boundaries.d.ts.map +1 -0
- package/dist/components/boundaries.js +55 -0
- package/dist/components/can.d.ts +11 -0
- package/dist/components/can.d.ts.map +1 -0
- package/dist/components/can.js +5 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +3 -0
- package/dist/graph/call/call.hook.d.ts +8 -18
- package/dist/graph/call/call.hook.d.ts.map +1 -1
- package/dist/graph/call/call.hook.js +7 -13
- package/dist/graph/call/stream.hook.d.ts +12 -20
- package/dist/graph/call/stream.hook.d.ts.map +1 -1
- package/dist/graph/call/stream.hook.js +55 -111
- package/dist/graph/graph.context.d.ts +4 -3
- package/dist/graph/graph.context.d.ts.map +1 -1
- package/dist/graph/graph.context.js +2 -8
- package/dist/graph/guards.error.d.ts +7 -8
- package/dist/graph/guards.error.d.ts.map +1 -1
- package/dist/graph/guards.error.js +11 -7
- package/dist/graph/index.d.ts +8 -16
- package/dist/graph/index.d.ts.map +1 -1
- package/dist/graph/index.js +3 -7
- package/dist/graph/memory/read/envelope.d.ts +22 -54
- package/dist/graph/memory/read/envelope.d.ts.map +1 -1
- package/dist/graph/memory/read/envelope.js +26 -20
- package/dist/graph/memory/read/idle.d.ts +1 -15
- package/dist/graph/memory/read/idle.d.ts.map +1 -1
- package/dist/graph/memory/read/idle.js +4 -13
- package/dist/graph/query-definition.hook.d.ts +11 -0
- package/dist/graph/query-definition.hook.d.ts.map +1 -0
- package/dist/graph/query-definition.hook.js +205 -0
- package/dist/graph/resource.d.ts +34 -0
- package/dist/graph/resource.d.ts.map +1 -0
- package/dist/graph/resource.js +244 -0
- package/dist/graph/store.d.ts +25 -0
- package/dist/graph/store.d.ts.map +1 -0
- package/dist/graph/store.js +69 -0
- package/dist/host/actions.hook.d.ts +34 -0
- package/dist/host/actions.hook.d.ts.map +1 -0
- package/dist/host/actions.hook.js +125 -0
- package/dist/host/components.d.ts +15 -0
- package/dist/host/components.d.ts.map +1 -0
- package/dist/host/components.js +14 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.d.ts.map +1 -0
- package/dist/host/index.js +3 -0
- package/dist/index.d.ts +26 -37
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -31
- package/dist/intents/envelope.d.ts +7 -19
- package/dist/intents/envelope.d.ts.map +1 -1
- package/dist/intents/envelope.js +2 -3
- package/dist/intents/send-intent.hook.d.ts +5 -7
- package/dist/intents/send-intent.hook.d.ts.map +1 -1
- package/dist/intents/send-intent.hook.js +17 -75
- package/dist/navigation/adapters/adapter.d.ts +5 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/memory.adapter.js +4 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -1
- package/dist/navigation/adapters/url.adapter.js +57 -10
- package/dist/navigation/codec.d.ts +60 -0
- package/dist/navigation/codec.d.ts.map +1 -0
- package/dist/navigation/codec.js +121 -0
- package/dist/navigation/history.d.ts +11 -9
- package/dist/navigation/history.d.ts.map +1 -1
- package/dist/navigation/history.js +6 -8
- package/dist/navigation/hooks/ancestors.hook.d.ts +5 -7
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/ancestors.hook.js +7 -51
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/location.hook.js +4 -6
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -1
- package/dist/navigation/hooks/scope.hook.js +4 -6
- package/dist/navigation/index.d.ts +4 -2
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js +2 -1
- package/dist/navigation/link.d.ts +12 -0
- package/dist/navigation/link.d.ts.map +1 -1
- package/dist/navigation/link.js +13 -2
- package/dist/navigation/location.d.ts +13 -40
- package/dist/navigation/location.d.ts.map +1 -1
- package/dist/navigation/location.js +14 -76
- package/dist/navigation/scope.context.d.ts +17 -26
- package/dist/navigation/scope.context.d.ts.map +1 -1
- package/dist/navigation/scope.context.js +140 -108
- package/dist/navigation/snapshot.d.ts +14 -19
- package/dist/navigation/snapshot.d.ts.map +1 -1
- package/dist/navigation/snapshot.js +48 -44
- package/dist/schema/action.hook.d.ts +44 -0
- package/dist/schema/action.hook.d.ts.map +1 -0
- package/dist/schema/action.hook.js +72 -0
- package/dist/schema/domain.hook.d.ts +12 -7
- package/dist/schema/domain.hook.d.ts.map +1 -1
- package/dist/schema/domain.hook.js +101 -4
- package/dist/schema/index.d.ts +9 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +5 -7
- package/dist/schema/kernel.hook.d.ts +4 -9
- package/dist/schema/kernel.hook.d.ts.map +1 -1
- package/dist/schema/kernel.hook.js +4 -6
- package/dist/schema/mutation.hook.d.ts +9 -0
- package/dist/schema/mutation.hook.d.ts.map +1 -0
- package/dist/schema/mutation.hook.js +41 -0
- package/dist/schema/object.hook.d.ts +16 -0
- package/dist/schema/object.hook.d.ts.map +1 -0
- package/dist/schema/object.hook.js +46 -0
- package/dist/schema/relation.hook.d.ts +46 -0
- package/dist/schema/relation.hook.d.ts.map +1 -0
- package/dist/schema/relation.hook.js +136 -0
- package/dist/selection/live.d.ts +9 -17
- package/dist/selection/live.d.ts.map +1 -1
- package/dist/selection/live.js +65 -112
- package/dist/selection/scope.context.js +5 -4
- package/dist/session/boot/boot.d.ts +1 -13
- package/dist/session/boot/boot.d.ts.map +1 -1
- package/dist/session/boot/boot.js +9 -27
- package/dist/session/index.d.ts +1 -0
- package/dist/session/index.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.d.ts +1 -4
- package/dist/session/kernel-instance.hook.d.ts.map +1 -1
- package/dist/session/kernel-instance.hook.js +1 -5
- package/dist/session/provider.d.ts +9 -23
- package/dist/session/provider.d.ts.map +1 -1
- package/dist/session/provider.js +29 -75
- package/dist/session/session.context.d.ts +5 -0
- package/dist/session/session.context.d.ts.map +1 -1
- package/dist/window/adopt.d.ts +0 -1
- package/dist/window/adopt.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.d.ts +10 -6
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -1
- package/dist/window/hooks/target-node.hook.js +14 -72
- package/dist/window/hooks/views-for.hook.d.ts +4 -3
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -1
- package/dist/window/hooks/views-for.hook.js +6 -22
- package/dist/window/hooks/windows.hook.d.ts.map +1 -1
- package/dist/window/hooks/windows.hook.js +3 -4
- package/dist/window/host.d.ts +9 -11
- package/dist/window/host.d.ts.map +1 -1
- package/dist/window/host.js +26 -62
- package/dist/window/hub.d.ts +2 -0
- package/dist/window/hub.d.ts.map +1 -1
- package/dist/window/hub.js +33 -0
- package/dist/window/index.d.ts +2 -2
- package/dist/window/index.d.ts.map +1 -1
- package/dist/window/index.js +2 -2
- package/dist/window/lifecycle/mount.d.ts +5 -44
- package/dist/window/lifecycle/mount.d.ts.map +1 -1
- package/dist/window/lifecycle/mount.js +14 -120
- package/dist/window/lifecycle/states.d.ts +0 -5
- package/dist/window/lifecycle/states.d.ts.map +1 -1
- package/package.json +20 -8
- package/src/application/apps/app-host.tsx +11 -15
- package/src/application/apps/apps.hook.ts +13 -6
- package/src/application/apps/domains.hook.ts +1 -1
- package/src/application/apps/open-app.hook.ts +3 -4
- package/src/application/index.ts +4 -6
- package/src/application/spaces/space.hook.ts +5 -6
- package/src/application/spaces/spaces.hook.ts +14 -9
- package/src/auth/act-as.context.tsx +37 -91
- package/src/auth/auth.hook.ts +2 -30
- package/src/auth/can.hook.ts +44 -135
- package/src/auth/index.ts +1 -0
- package/src/auth/self.hook.ts +2 -9
- package/src/auth/session-kernel.ts +6 -16
- package/src/components/astrale.tsx +40 -0
- package/src/components/boundaries.tsx +113 -0
- package/src/components/can.tsx +16 -0
- package/src/components/index.ts +13 -0
- package/src/graph/call/call.hook.ts +22 -27
- package/src/graph/call/stream.hook.ts +66 -143
- package/src/graph/graph.context.ts +6 -11
- package/src/graph/guards.error.ts +17 -55
- package/src/graph/index.ts +23 -17
- package/src/graph/memory/read/envelope.ts +40 -65
- package/src/graph/memory/read/idle.ts +5 -18
- package/src/graph/query-definition.hook.ts +270 -0
- package/src/graph/resource.ts +358 -0
- package/src/graph/store.ts +97 -0
- package/src/host/actions.hook.ts +165 -0
- package/src/host/components.tsx +29 -0
- package/src/host/index.ts +17 -0
- package/src/index.ts +131 -124
- package/src/intents/envelope.ts +7 -23
- package/src/intents/send-intent.hook.ts +24 -79
- package/src/navigation/adapters/adapter.ts +3 -0
- package/src/navigation/adapters/memory.adapter.ts +4 -0
- package/src/navigation/adapters/url.adapter.ts +48 -8
- package/src/navigation/codec.ts +190 -0
- package/src/navigation/history.ts +20 -11
- package/src/navigation/hooks/ancestors.hook.ts +10 -60
- package/src/navigation/hooks/location.hook.ts +4 -6
- package/src/navigation/hooks/scope.hook.ts +4 -6
- package/src/navigation/index.ts +18 -2
- package/src/navigation/link.tsx +27 -2
- package/src/navigation/location.ts +26 -93
- package/src/navigation/scope.context.tsx +151 -136
- package/src/navigation/snapshot.ts +69 -50
- package/src/schema/action.hook.ts +146 -0
- package/src/schema/domain.hook.ts +127 -11
- package/src/schema/index.ts +9 -30
- package/src/schema/kernel.hook.ts +6 -13
- package/src/schema/mutation.hook.ts +63 -0
- package/src/schema/object.hook.ts +85 -0
- package/src/schema/relation.hook.ts +253 -0
- package/src/selection/live.ts +75 -125
- package/src/selection/scope.context.tsx +4 -4
- package/src/session/boot/boot.ts +8 -33
- package/src/session/index.ts +1 -0
- package/src/session/kernel-instance.hook.ts +2 -11
- package/src/session/provider.tsx +34 -103
- package/src/session/session.context.ts +6 -0
- package/src/window/adopt.ts +0 -1
- package/src/window/hooks/target-node.hook.ts +20 -85
- package/src/window/hooks/views-for.hook.ts +12 -25
- package/src/window/hooks/windows.hook.ts +3 -4
- package/src/window/host.tsx +35 -77
- package/src/window/hub.ts +33 -0
- package/src/window/index.ts +3 -3
- package/src/window/lifecycle/mount.ts +15 -160
- package/src/window/lifecycle/states.ts +1 -5
- package/dist/application/roles.hook.d.ts +0 -7
- package/dist/application/roles.hook.d.ts.map +0 -1
- package/dist/application/roles.hook.js +0 -23
- package/dist/graph/memory/base.d.ts +0 -21
- package/dist/graph/memory/base.d.ts.map +0 -1
- package/dist/graph/memory/base.js +0 -33
- package/dist/graph/memory/read/read.d.ts +0 -9
- package/dist/graph/memory/read/read.d.ts.map +0 -1
- package/dist/graph/memory/read/read.js +0 -87
- package/dist/graph/memory/scope.d.ts +0 -7
- package/dist/graph/memory/scope.d.ts.map +0 -1
- package/dist/graph/memory/scope.js +0 -25
- package/dist/graph/mutate/move.hook.d.ts +0 -7
- package/dist/graph/mutate/move.hook.d.ts.map +0 -1
- package/dist/graph/mutate/move.hook.js +0 -49
- package/dist/graph/mutate/mutate.hook.d.ts +0 -17
- package/dist/graph/mutate/mutate.hook.d.ts.map +0 -1
- package/dist/graph/mutate/mutate.hook.js +0 -45
- package/dist/graph/query/children.hook.d.ts +0 -29
- package/dist/graph/query/children.hook.d.ts.map +0 -1
- package/dist/graph/query/children.hook.js +0 -64
- package/dist/graph/query/graph.hook.d.ts +0 -9
- package/dist/graph/query/graph.hook.d.ts.map +0 -1
- package/dist/graph/query/graph.hook.js +0 -5
- package/dist/graph/query/neighbors.hook.d.ts +0 -7
- package/dist/graph/query/neighbors.hook.d.ts.map +0 -1
- package/dist/graph/query/neighbors.hook.js +0 -104
- package/dist/graph/query/node.hook.d.ts +0 -14
- package/dist/graph/query/node.hook.d.ts.map +0 -1
- package/dist/graph/query/node.hook.js +0 -43
- package/dist/graph/query/query.hook.d.ts +0 -14
- package/dist/graph/query/query.hook.d.ts.map +0 -1
- package/dist/graph/query/query.hook.js +0 -23
- package/dist/schema/bind-read.d.ts +0 -37
- package/dist/schema/bind-read.d.ts.map +0 -1
- package/dist/schema/bind-read.js +0 -205
- package/dist/schema/binding.d.ts +0 -21
- package/dist/schema/binding.d.ts.map +0 -1
- package/dist/schema/binding.js +0 -99
- package/dist/schema/function.hook.d.ts +0 -9
- package/dist/schema/function.hook.d.ts.map +0 -1
- package/dist/schema/function.hook.js +0 -40
- package/dist/schema/method.hook.d.ts +0 -14
- package/dist/schema/method.hook.d.ts.map +0 -1
- package/dist/schema/method.hook.js +0 -49
- package/dist/schema/tag.d.ts +0 -11
- package/dist/schema/tag.d.ts.map +0 -1
- package/dist/schema/tag.js +0 -11
- package/dist/schema/types.d.ts +0 -73
- package/dist/schema/types.d.ts.map +0 -1
- package/dist/schema/types.js +0 -1
- package/dist/schema/write.d.ts +0 -16
- package/dist/schema/write.d.ts.map +0 -1
- package/dist/schema/write.js +0 -56
- package/src/application/roles.hook.ts +0 -41
- package/src/graph/memory/base.ts +0 -50
- package/src/graph/memory/read/read.ts +0 -103
- package/src/graph/memory/scope.ts +0 -29
- package/src/graph/mutate/move.hook.ts +0 -70
- package/src/graph/mutate/mutate.hook.ts +0 -69
- package/src/graph/query/children.hook.ts +0 -136
- package/src/graph/query/graph.hook.ts +0 -13
- package/src/graph/query/neighbors.hook.ts +0 -159
- package/src/graph/query/node.hook.ts +0 -83
- package/src/graph/query/query.hook.ts +0 -42
- package/src/schema/bind-read.ts +0 -319
- package/src/schema/binding.ts +0 -146
- package/src/schema/function.hook.ts +0 -68
- package/src/schema/method.hook.ts +0 -103
- package/src/schema/tag.ts +0 -19
- package/src/schema/types.ts +0 -150
- package/src/schema/write.ts +0 -96
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { GraphStore, StoreQueryResponse } from '@astrale-os/kernel-client/store'
|
|
2
2
|
import type { ReactElement, ReactNode } from 'react'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* stack against the memory, mirrors navigations to/from the adapter, and runs the
|
|
8
|
-
* `missing` policy when the current anchor stops resolving. Also the scope's shared
|
|
9
|
-
* glue (`NavContext`, the `useSyncExternalStore` reads) and the imperative
|
|
10
|
-
* `Navigate` surface. Scopes nest.
|
|
11
|
-
*/
|
|
4
|
+
import { NodePath } from '@astrale-os/sdk/graph/path'
|
|
5
|
+
import { Query } from '@astrale-os/sdk/query'
|
|
6
|
+
import { Key } from '@astrale-os/sdk/schema'
|
|
12
7
|
import {
|
|
13
8
|
createContext,
|
|
14
9
|
startTransition,
|
|
@@ -22,40 +17,40 @@ import {
|
|
|
22
17
|
|
|
23
18
|
import type { NavAdapter } from './adapters/adapter.js'
|
|
24
19
|
import type { NavSnapshot, SerializedLocation } from './history.js'
|
|
25
|
-
import type { NavLocation } from './location.js'
|
|
20
|
+
import type { NavLocation, PointResult } from './location.js'
|
|
26
21
|
import type { NavTarget } from './snapshot.js'
|
|
27
22
|
|
|
28
|
-
import {
|
|
23
|
+
import { UnsupportedFeatureError } from '../graph/memory/read/envelope.js'
|
|
24
|
+
import { useStoreResource } from '../graph/resource.js'
|
|
25
|
+
import { useGraphStore } from '../graph/store.js'
|
|
26
|
+
import { useShell } from '../session/shell.hook.js'
|
|
29
27
|
import { memoryAdapter } from './adapters/memory.adapter.js'
|
|
30
28
|
import { NavHistory, seedEntries } from './history.js'
|
|
31
|
-
import { buildLocation
|
|
29
|
+
import { buildLocation } from './location.js'
|
|
32
30
|
import { revalidateAndPrune, serializeTarget } from './snapshot.js'
|
|
33
31
|
|
|
34
|
-
/** The imperative navigation surface (API §8.2). */
|
|
35
32
|
export interface Navigate {
|
|
36
33
|
(to: NavTarget, opts?: { replace?: boolean }): void
|
|
37
34
|
back(): void
|
|
38
35
|
forward(): void
|
|
39
|
-
/**
|
|
40
|
-
up():
|
|
36
|
+
/** Unsupported until the caller supplies an explicit parent Edge Class. */
|
|
37
|
+
up(): never
|
|
41
38
|
readonly canBack: boolean
|
|
42
39
|
readonly canForward: boolean
|
|
43
40
|
}
|
|
44
41
|
|
|
45
|
-
/** A mutable `Navigate` — the scope stamps `canBack`/`canForward` onto it live. */
|
|
46
42
|
export interface MutableNavigate {
|
|
47
43
|
(to: Parameters<Navigate>[0], opts?: Parameters<Navigate>[1]): void
|
|
48
44
|
back(): void
|
|
49
45
|
forward(): void
|
|
50
|
-
up():
|
|
46
|
+
up(): never
|
|
51
47
|
canBack: boolean
|
|
52
48
|
canForward: boolean
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
/** The nearest scope's shared handle (stable for the scope's lifetime). */
|
|
56
51
|
export interface NavCtx {
|
|
57
52
|
readonly history: NavHistory
|
|
58
|
-
readonly
|
|
53
|
+
readonly store: GraphStore
|
|
59
54
|
readonly adapter: NavAdapter
|
|
60
55
|
readonly navigate: MutableNavigate
|
|
61
56
|
}
|
|
@@ -63,205 +58,225 @@ export interface NavCtx {
|
|
|
63
58
|
export const NavContext = createContext<NavCtx | null>(null)
|
|
64
59
|
NavContext.displayName = 'AstraleNavContext'
|
|
65
60
|
|
|
66
|
-
/** The nearest scope, or throw — the door every public nav hook goes through. */
|
|
67
61
|
export function useNavCtx(): NavCtx {
|
|
68
62
|
const ctx = useContext(NavContext)
|
|
69
63
|
if (ctx === null) throw new Error('nav hooks must be used within a <NavScope>')
|
|
70
64
|
return ctx
|
|
71
65
|
}
|
|
72
66
|
|
|
73
|
-
/** The nearest scope or null (for `useAncestors`, usable outside a scope). */
|
|
74
67
|
export function useOptionalNavCtx(): NavCtx | null {
|
|
75
68
|
return useContext(NavContext)
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
const NOOP_UNSUB = (): void => undefined
|
|
79
72
|
|
|
80
|
-
/** Re-render on any history change (no-op subscription when there is no scope). */
|
|
81
73
|
export function useHistoryVersion(history: NavHistory | null): number {
|
|
82
74
|
const subscribe = useCallback(
|
|
83
|
-
(
|
|
75
|
+
(listener: () => void) => (history === null ? NOOP_UNSUB : history.subscribe(listener)),
|
|
84
76
|
[history],
|
|
85
77
|
)
|
|
86
|
-
const getSnapshot = useCallback(() => (history
|
|
78
|
+
const getSnapshot = useCallback(() => (history === null ? 0 : history.getVersion()), [history])
|
|
87
79
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
88
80
|
}
|
|
89
81
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
/** Caller-scoped canonical point read through the provider's one GraphStore. */
|
|
83
|
+
export function useGraphPoint(target: NodePath | null): PointResult | null {
|
|
84
|
+
const query = useMemo(
|
|
85
|
+
() =>
|
|
86
|
+
target === null
|
|
87
|
+
? null
|
|
88
|
+
: Query.from({ kind: 'node', paths: [target], binding: 'navigation_target' }).select({
|
|
89
|
+
kind: 'graph',
|
|
90
|
+
}),
|
|
91
|
+
[target],
|
|
98
92
|
)
|
|
99
|
-
const
|
|
100
|
-
(
|
|
101
|
-
|
|
93
|
+
const project = useCallback(
|
|
94
|
+
(response: StoreQueryResponse<NonNullable<typeof query>>) => {
|
|
95
|
+
const result = response.result
|
|
96
|
+
if (result.kind !== 'graph' || result.selection.kind !== 'node') return null
|
|
97
|
+
const id = result.selection.ids[0]
|
|
98
|
+
return id === undefined ? null : (result.graph.nodes.find((node) => node.id === id) ?? null)
|
|
99
|
+
},
|
|
100
|
+
[query],
|
|
102
101
|
)
|
|
103
|
-
|
|
102
|
+
const resource = useStoreResource(query, 1, project)
|
|
103
|
+
if (target === null) return null
|
|
104
|
+
return {
|
|
105
|
+
node: resource.data ?? null,
|
|
106
|
+
pending: resource.state === 'loading' || resource.state === 'refreshing',
|
|
107
|
+
error: resource.error,
|
|
108
|
+
live: false,
|
|
109
|
+
seq: 0,
|
|
110
|
+
refetch: resource.refresh,
|
|
111
|
+
}
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
export type MissingPolicy = '
|
|
114
|
+
type MissingResolver = (location: NavLocation) => NavTarget | null
|
|
115
|
+
export type MissingPolicy = 'hold' | MissingResolver
|
|
108
116
|
|
|
109
117
|
export interface NavScopeProps {
|
|
110
|
-
/** Persistence seam. Default: a fresh {@link memoryAdapter} (captured once). */
|
|
111
118
|
adapter?: NavAdapter
|
|
112
|
-
/** A saved snapshot — revalidated against the memory; dead entries dropped. */
|
|
113
119
|
restore?: NavSnapshot
|
|
114
|
-
/** Seed target when neither `adapter.read()` nor `restore` yields one. */
|
|
115
120
|
initial?: NavTarget
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/** What to do when the current anchor stops resolving. Default `'ancestor'`. */
|
|
121
|
+
onChange?: (location: NavLocation, snapshot: NavSnapshot) => void
|
|
122
|
+
/** Default `hold`; parent fallback requires an explicit caller relationship. */
|
|
119
123
|
missing?: MissingPolicy
|
|
124
|
+
/** Closed codec admission applied before any history or adapter mutation. */
|
|
125
|
+
admit?: (location: SerializedLocation) => SerializedLocation | null
|
|
120
126
|
children: ReactNode
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
export function NavScope(props: NavScopeProps): ReactElement {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
// Adapter + history are captured ONCE — an inline `adapter={memoryAdapter()}`
|
|
127
|
-
// (or a re-passed `initial`) must never rebuild the scope's state.
|
|
130
|
+
const store = useGraphStore()
|
|
131
|
+
const shell = useShell()
|
|
128
132
|
const adapterRef = useRef<NavAdapter | null>(null)
|
|
129
133
|
if (adapterRef.current === null) adapterRef.current = props.adapter ?? memoryAdapter()
|
|
130
134
|
const adapter = adapterRef.current
|
|
135
|
+
if (adapter.kind === 'browser' && shell.hostCapabilities.navigation.history !== 'browser') {
|
|
136
|
+
throw new Error('Browser navigation is not granted to this Shell.')
|
|
137
|
+
}
|
|
138
|
+
const admitRef = useRef(props.admit)
|
|
139
|
+
admitRef.current = props.admit
|
|
140
|
+
const admit = (location: SerializedLocation | null): SerializedLocation | null => {
|
|
141
|
+
if (location === null) return null
|
|
142
|
+
const portable = admitLocation(location)
|
|
143
|
+
return admitRef.current?.(portable) ?? portable
|
|
144
|
+
}
|
|
131
145
|
|
|
132
146
|
const historyRef = useRef<NavHistory | null>(null)
|
|
133
147
|
if (historyRef.current === null) {
|
|
134
148
|
const seed = seedEntries(
|
|
135
|
-
adapter.read(),
|
|
136
|
-
props.restore
|
|
137
|
-
|
|
149
|
+
admit(adapter.read()),
|
|
150
|
+
props.restore === undefined || props.restore.v !== 1
|
|
151
|
+
? undefined
|
|
152
|
+
: {
|
|
153
|
+
entries: props.restore.entries.flatMap((entry) => {
|
|
154
|
+
const accepted = admit(entry)
|
|
155
|
+
return accepted === null ? [] : [accepted]
|
|
156
|
+
}),
|
|
157
|
+
cursor: props.restore.cursor,
|
|
158
|
+
},
|
|
159
|
+
props.initial === undefined ? null : admit(serializeTarget(props.initial)),
|
|
138
160
|
)
|
|
139
161
|
historyRef.current = new NavHistory(seed.entries, seed.cursor)
|
|
140
162
|
}
|
|
141
163
|
const history = historyRef.current
|
|
142
|
-
|
|
143
|
-
// Latest-value refs so the stable navigate + effects read current inputs.
|
|
144
|
-
const memoryRef = useRef(memory)
|
|
145
|
-
memoryRef.current = memory
|
|
146
164
|
const onChangeRef = useRef(props.onChange)
|
|
147
165
|
onChangeRef.current = props.onChange
|
|
148
166
|
const missingRef = useRef(props.missing)
|
|
149
167
|
missingRef.current = props.missing
|
|
168
|
+
const historyVersion = useHistoryVersion(history)
|
|
150
169
|
|
|
151
170
|
const fireOnChange = useCallback((): void => {
|
|
152
|
-
|
|
153
|
-
if (cb === undefined) return
|
|
154
|
-
const entry = history.current
|
|
155
|
-
const live = entry !== null ? memoryRef.current.peek(entry.node) : null
|
|
156
|
-
cb(buildLocation(entry, live, (live?.node ?? null) !== null), history.snapshot())
|
|
171
|
+
onChangeRef.current?.(buildLocation(history.current, null), history.snapshot())
|
|
157
172
|
}, [history])
|
|
158
173
|
|
|
159
|
-
// The one stable navigate object; hooks re-stamp canBack/canForward onto it.
|
|
160
174
|
const navigate = useMemo<MutableNavigate>(() => {
|
|
161
175
|
const commit = (changed: boolean, replace: boolean): void => {
|
|
162
176
|
if (!changed) return
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
177
|
+
const location = history.current
|
|
178
|
+
if (location !== null) adapter.write(location, { replace })
|
|
165
179
|
fireOnChange()
|
|
166
180
|
}
|
|
167
|
-
const nav = ((to: NavTarget,
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
181
|
+
const nav = ((to: NavTarget, options?: { replace?: boolean }) => {
|
|
182
|
+
const replace = options?.replace ?? false
|
|
183
|
+
const location = admit(serializeTarget(to))
|
|
184
|
+
if (location === null) throw new TypeError('Navigation target was rejected by its codec.')
|
|
185
|
+
startTransition(() => commit(history.push(location, replace), replace))
|
|
171
186
|
}) as MutableNavigate
|
|
172
187
|
nav.back = () => startTransition(() => commit(history.back(), false))
|
|
173
188
|
nav.forward = () => startTransition(() => commit(history.forward(), false))
|
|
174
|
-
nav.up = () => {
|
|
175
|
-
|
|
176
|
-
if (entry === null) return
|
|
177
|
-
const live = memoryRef.current.peek(entry.node)
|
|
178
|
-
const raw = live?.node ? pathOf(live.node) : pathHint(entry.node)
|
|
179
|
-
const parent = raw !== '' ? parentPath(raw) : null
|
|
180
|
-
if (parent !== null) nav(parent)
|
|
189
|
+
nav.up = (): never => {
|
|
190
|
+
throw new UnsupportedFeatureError('navigation.up requires an explicit parent Edge Class')
|
|
181
191
|
}
|
|
182
192
|
nav.canBack = history.canBack
|
|
183
193
|
nav.canForward = history.canForward
|
|
184
194
|
return nav
|
|
185
|
-
}, [history, adapter, fireOnChange])
|
|
186
|
-
|
|
187
|
-
// Re-render the scope on every navigation so the missing watcher tracks the
|
|
188
|
-
// current entry (consumers re-render through their own version subscription).
|
|
189
|
-
const historyVersion = useHistoryVersion(history)
|
|
195
|
+
}, [history, adapter, fireOnChange, historyVersion])
|
|
190
196
|
|
|
191
|
-
// Mount: canonicalize a non-adapter seed into the adapter, then relay external
|
|
192
|
-
// (browser/URL) changes back into the history.
|
|
193
197
|
useEffect(() => {
|
|
194
|
-
const
|
|
195
|
-
if (
|
|
198
|
+
const seed = history.current
|
|
199
|
+
if (seed !== null && adapter.read() === null) adapter.write(seed, { replace: true })
|
|
196
200
|
return adapter.subscribe(() => {
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
201
|
+
const location = admit(adapter.read())
|
|
202
|
+
if (location === null) return
|
|
199
203
|
startTransition(() => {
|
|
200
|
-
if (history.syncExternal(
|
|
204
|
+
if (history.syncExternal(location)) fireOnChange()
|
|
201
205
|
})
|
|
202
206
|
})
|
|
203
207
|
}, [history, adapter, fireOnChange])
|
|
204
208
|
|
|
205
|
-
// Restore revalidation — once, against the shared memory.
|
|
206
|
-
const revalidatedRef = useRef(false)
|
|
207
209
|
useEffect(() => {
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
void revalidateAndPrune(history,
|
|
211
|
-
|
|
210
|
+
if (props.restore === undefined) return
|
|
211
|
+
let current = true
|
|
212
|
+
void revalidateAndPrune(history, store, shell).then((changed) => {
|
|
213
|
+
if (!current || !changed) return
|
|
214
|
+
const accepted = history.current
|
|
215
|
+
if (accepted === null) adapter.clear({ replace: true })
|
|
216
|
+
else adapter.write(accepted, { replace: true })
|
|
217
|
+
fireOnChange()
|
|
218
|
+
})
|
|
219
|
+
return () => {
|
|
220
|
+
current = false
|
|
221
|
+
}
|
|
222
|
+
}, [store, shell, history, adapter, props.restore, fireOnChange])
|
|
212
223
|
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
const settled = useSettled(currentResult, currentShape)
|
|
217
|
-
const lastPathRef = useRef<string | null>(null)
|
|
218
|
-
const handledRef = useRef<string | null>(null)
|
|
224
|
+
const current = history.current
|
|
225
|
+
const result = useGraphPoint(current?.node ?? null)
|
|
226
|
+
const handled = useRef<NodePath | null>(null)
|
|
219
227
|
useEffect(() => {
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
useEffect(() => {
|
|
224
|
-
const entry = history.current
|
|
225
|
-
if (entry === null) return
|
|
226
|
-
if ((currentResult?.node ?? null) !== null) {
|
|
227
|
-
handledRef.current = null
|
|
228
|
+
if (current === null || result === null || result.pending) return
|
|
229
|
+
if (result.node !== null) {
|
|
230
|
+
handled.current = null
|
|
228
231
|
return
|
|
229
232
|
}
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
navigate,
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
}, [settled, currentResult, history, navigate, historyVersion])
|
|
233
|
+
if (handled.current === current.node) return
|
|
234
|
+
handled.current = current.node
|
|
235
|
+
const policy = missingRef.current ?? 'hold'
|
|
236
|
+
if (typeof policy === 'function') {
|
|
237
|
+
const target = policy(buildLocation(current, result))
|
|
238
|
+
if (target !== null) navigate(target, { replace: true })
|
|
239
|
+
}
|
|
240
|
+
}, [current, result, navigate, historyVersion])
|
|
239
241
|
|
|
240
242
|
const ctx = useMemo<NavCtx>(
|
|
241
|
-
() => ({ history,
|
|
242
|
-
[history,
|
|
243
|
+
() => ({ history, store, adapter, navigate }),
|
|
244
|
+
[history, store, adapter, navigate],
|
|
243
245
|
)
|
|
244
|
-
|
|
245
246
|
return <NavContext.Provider value={ctx}>{props.children}</NavContext.Provider>
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
function
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
249
|
+
function admitLocation(location: SerializedLocation): SerializedLocation {
|
|
250
|
+
if (location === null || typeof location !== 'object' || Array.isArray(location)) {
|
|
251
|
+
throw new TypeError('Navigation location is invalid.')
|
|
252
|
+
}
|
|
253
|
+
const node = NodePath(location.node)
|
|
254
|
+
let view: Key.View | undefined
|
|
255
|
+
if (location.view !== undefined) {
|
|
256
|
+
const key = Key(location.view)
|
|
257
|
+
if (!Key.ref(key).startsWith('view.')) throw new TypeError('Navigation View key is invalid.')
|
|
258
|
+
view = key as Key.View
|
|
259
|
+
}
|
|
260
|
+
const params: Record<string, string> = {}
|
|
261
|
+
if (location.params !== undefined) {
|
|
262
|
+
if (
|
|
263
|
+
location.params === null ||
|
|
264
|
+
typeof location.params !== 'object' ||
|
|
265
|
+
Array.isArray(location.params)
|
|
266
|
+
) {
|
|
267
|
+
throw new TypeError('Navigation params are invalid.')
|
|
268
|
+
}
|
|
269
|
+
for (const [key, value] of Object.entries(location.params)) {
|
|
270
|
+
if (/token|credential|authorization|secret/i.test(key)) {
|
|
271
|
+
throw new TypeError(`Navigation param '${key}' is sensitive.`)
|
|
272
|
+
}
|
|
273
|
+
if (typeof value !== 'string') throw new TypeError(`Navigation param '${key}' is invalid.`)
|
|
274
|
+
params[key] = value
|
|
275
|
+
}
|
|
262
276
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
277
|
+
return Object.freeze({
|
|
278
|
+
node,
|
|
279
|
+
...(view === undefined ? {} : { view }),
|
|
280
|
+
...(Object.keys(params).length === 0 ? {} : { params: Object.freeze(params) }),
|
|
281
|
+
})
|
|
267
282
|
}
|
|
@@ -1,78 +1,97 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { GraphStore } from '@astrale-os/kernel-client/store'
|
|
2
|
+
import type { Node } from '@astrale-os/sdk/graph/node'
|
|
3
|
+
import type { NodePath, PathLike } from '@astrale-os/sdk/graph/path'
|
|
4
|
+
import type { Key } from '@astrale-os/sdk/schema'
|
|
5
|
+
import type { Shell } from '@astrale-os/shell'
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* restore-time `revalidateAndPrune` that drops dead entries against the memory.
|
|
9
|
-
*/
|
|
10
|
-
import { normalizeRef } from '@astrale-os/kernel-client'
|
|
7
|
+
import { Path } from '@astrale-os/sdk/graph/path'
|
|
8
|
+
import { Query } from '@astrale-os/sdk/query'
|
|
9
|
+
import { Key as admitKey } from '@astrale-os/sdk/schema'
|
|
11
10
|
|
|
12
|
-
import type { NavHistory } from './history.js'
|
|
13
|
-
import type { SerializedLocation } from './history.js'
|
|
14
|
-
import type { Node, NodeId } from './location.js'
|
|
11
|
+
import type { NavHistory, SerializedLocation } from './history.js'
|
|
15
12
|
|
|
16
|
-
/**
|
|
13
|
+
/** Canonical navigation inputs; there is no implicit slash-tree or bare-id grammar. */
|
|
17
14
|
export type NavTarget =
|
|
18
|
-
| NodeId
|
|
19
15
|
| PathLike
|
|
20
16
|
| Node
|
|
21
|
-
|
|
|
22
|
-
|
|
17
|
+
| {
|
|
18
|
+
readonly node: PathLike | Node
|
|
19
|
+
readonly view?: Key.View
|
|
20
|
+
readonly params?: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isNode(value: PathLike | Node): value is Node {
|
|
24
|
+
return typeof value === 'object' && value !== null && 'id' in value && 'class' in value
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nodePath(value: PathLike | Node): NodePath {
|
|
28
|
+
return isNode(value) ? Path.id(value.id).raw : Path.from(value).raw
|
|
29
|
+
}
|
|
23
30
|
|
|
24
|
-
function
|
|
25
|
-
|
|
31
|
+
function isComposite(
|
|
32
|
+
value: NavTarget,
|
|
33
|
+
): value is Extract<NavTarget, { readonly node: PathLike | Node }> {
|
|
34
|
+
return typeof value === 'object' && value !== null && 'node' in value
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
/** Normalize
|
|
37
|
+
/** Normalize every target to canonical astrale-node-path-v1 text. */
|
|
29
38
|
export function serializeTarget(target: NavTarget): SerializedLocation {
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
...(t.view !== undefined ? { view: normalizeRef(t.view) } : {}),
|
|
36
|
-
...(t.params !== undefined ? { params: { ...t.params } } : {}),
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
// A Node / BoundNode carries `id` (id-anchored, so rename/move survive) and `path`;
|
|
40
|
-
// anything else is a typed `Path` value → its raw address.
|
|
41
|
-
if (isRecord(target) && 'id' in target && 'path' in target) {
|
|
42
|
-
return { node: normalizeRef((target as { id: NodeRefInput }).id) }
|
|
39
|
+
if (!isComposite(target)) return { node: nodePath(target) }
|
|
40
|
+
return {
|
|
41
|
+
node: nodePath(target.node),
|
|
42
|
+
...(target.view === undefined ? {} : { view: viewKey(target.view) }),
|
|
43
|
+
...(target.params === undefined ? {} : { params: { ...target.params } }),
|
|
43
44
|
}
|
|
44
|
-
return { node: normalizeRef(target as NodeRefInput) }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
function viewKey(input: string): Key.View {
|
|
48
|
+
const key = admitKey(input)
|
|
49
|
+
if (!admitKey.ref(key).startsWith('view.')) throw new TypeError(`Invalid View key '${input}'.`)
|
|
50
|
+
return key as Key.View
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Drop restored entries that no longer resolve under the authenticated Graph capability. */
|
|
54
|
+
export async function revalidateAndPrune(
|
|
55
|
+
history: NavHistory,
|
|
56
|
+
store: GraphStore,
|
|
57
|
+
shell: Shell,
|
|
58
|
+
): Promise<boolean> {
|
|
54
59
|
const { entries, cursor } = history.snapshot()
|
|
55
|
-
const alive = await Promise.all(entries.map((
|
|
56
|
-
if (alive.every(Boolean)) return
|
|
60
|
+
const alive = await Promise.all(entries.map((entry) => resolves(store, shell, entry)))
|
|
61
|
+
if (alive.every(Boolean)) return false
|
|
57
62
|
|
|
58
63
|
const survivors: SerializedLocation[] = []
|
|
59
64
|
let newCursor = -1
|
|
60
65
|
let fallback = -1
|
|
61
|
-
entries.forEach((entry,
|
|
62
|
-
if (!alive[
|
|
66
|
+
entries.forEach((entry, index) => {
|
|
67
|
+
if (!alive[index]) return
|
|
63
68
|
survivors.push(entry)
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
else if (
|
|
69
|
+
const next = survivors.length - 1
|
|
70
|
+
if (index === cursor) newCursor = next
|
|
71
|
+
else if (index < cursor) fallback = next
|
|
67
72
|
})
|
|
68
73
|
if (newCursor === -1) newCursor = fallback !== -1 ? fallback : survivors.length > 0 ? 0 : -1
|
|
69
74
|
history.reset(survivors, newCursor)
|
|
75
|
+
return true
|
|
70
76
|
}
|
|
71
77
|
|
|
72
|
-
async function resolves(
|
|
78
|
+
async function resolves(
|
|
79
|
+
store: GraphStore,
|
|
80
|
+
shell: Shell,
|
|
81
|
+
entry: SerializedLocation,
|
|
82
|
+
): Promise<boolean> {
|
|
73
83
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
84
|
+
const query = Query.from({ kind: 'node', paths: [entry.node] }).select({ kind: 'graph' })
|
|
85
|
+
const response = await store.read(query, { page: { size: 1 } })
|
|
86
|
+
if (
|
|
87
|
+
response.result.kind !== 'graph' ||
|
|
88
|
+
response.result.selection.kind !== 'node' ||
|
|
89
|
+
response.result.selection.ids.length === 0
|
|
90
|
+
)
|
|
91
|
+
return false
|
|
92
|
+
if (entry.view === undefined) return true
|
|
93
|
+
const routes = await shell.views.resolve(entry.node)
|
|
94
|
+
return routes.some((candidate) => candidate.route.key === entry.view)
|
|
76
95
|
} catch {
|
|
77
96
|
return false
|
|
78
97
|
}
|