@astrale-os/shell-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/application/apps/app-host.d.ts +31 -0
- package/dist/application/apps/app-host.d.ts.map +1 -0
- package/dist/application/apps/app-host.js +59 -0
- package/dist/application/apps/app-logo.d.ts +19 -0
- package/dist/application/apps/app-logo.d.ts.map +1 -0
- package/dist/application/apps/app-logo.js +31 -0
- package/dist/application/apps/apps.hook.d.ts +12 -0
- package/dist/application/apps/apps.hook.d.ts.map +1 -0
- package/dist/application/apps/apps.hook.js +51 -0
- package/dist/application/apps/open-app.hook.d.ts +10 -0
- package/dist/application/apps/open-app.hook.d.ts.map +1 -0
- package/dist/application/apps/open-app.hook.js +7 -0
- package/dist/application/index.d.ts +18 -0
- package/dist/application/index.d.ts.map +1 -0
- package/dist/application/index.js +12 -0
- package/dist/application/roles.hook.d.ts +7 -0
- package/dist/application/roles.hook.d.ts.map +1 -0
- package/dist/application/roles.hook.js +23 -0
- package/dist/application/spaces/current-space.hook.d.ts +7 -0
- package/dist/application/spaces/current-space.hook.d.ts.map +1 -0
- package/dist/application/spaces/current-space.hook.js +27 -0
- package/dist/application/spaces/current-space.store.d.ts +32 -0
- package/dist/application/spaces/current-space.store.d.ts.map +1 -0
- package/dist/application/spaces/current-space.store.js +79 -0
- package/dist/application/spaces/space.hook.d.ts +5 -0
- package/dist/application/spaces/space.hook.d.ts.map +1 -0
- package/dist/application/spaces/space.hook.js +25 -0
- package/dist/application/spaces/spaces.hook.d.ts +14 -0
- package/dist/application/spaces/spaces.hook.d.ts.map +1 -0
- package/dist/application/spaces/spaces.hook.js +82 -0
- package/dist/application/user/user.hook.d.ts +10 -0
- package/dist/application/user/user.hook.d.ts.map +1 -0
- package/dist/application/user/user.hook.js +19 -0
- package/dist/application/user/user.store.d.ts +35 -0
- package/dist/application/user/user.store.d.ts.map +1 -0
- package/dist/application/user/user.store.js +51 -0
- package/dist/auth/act-as.context.d.ts +40 -0
- package/dist/auth/act-as.context.d.ts.map +1 -0
- package/dist/auth/act-as.context.js +84 -0
- package/dist/auth/auth.hook.d.ts +3 -0
- package/dist/auth/auth.hook.d.ts.map +1 -0
- package/dist/auth/auth.hook.js +25 -0
- package/dist/auth/can.hook.d.ts +40 -0
- package/dist/auth/can.hook.d.ts.map +1 -0
- package/dist/auth/can.hook.js +119 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +5 -0
- package/dist/auth/self.hook.d.ts +3 -0
- package/dist/auth/self.hook.d.ts.map +1 -0
- package/dist/auth/self.hook.js +11 -0
- package/dist/auth/session-kernel.d.ts +4 -0
- package/dist/auth/session-kernel.d.ts.map +1 -0
- package/dist/auth/session-kernel.js +21 -0
- package/dist/graph/action.d.ts +22 -0
- package/dist/graph/action.d.ts.map +1 -0
- package/dist/graph/action.js +85 -0
- package/dist/graph/async.d.ts +11 -0
- package/dist/graph/async.d.ts.map +1 -0
- package/dist/graph/async.js +60 -0
- package/dist/graph/call/call.hook.d.ts +21 -0
- package/dist/graph/call/call.hook.d.ts.map +1 -0
- package/dist/graph/call/call.hook.js +23 -0
- package/dist/graph/call/stream.hook.d.ts +28 -0
- package/dist/graph/call/stream.hook.d.ts.map +1 -0
- package/dist/graph/call/stream.hook.js +137 -0
- package/dist/graph/graph.context.d.ts +4 -0
- package/dist/graph/graph.context.d.ts.map +1 -0
- package/dist/graph/graph.context.js +10 -0
- package/dist/graph/guards.error.d.ts +9 -0
- package/dist/graph/guards.error.d.ts.map +1 -0
- package/dist/graph/guards.error.js +7 -0
- package/dist/graph/index.d.ts +20 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +9 -0
- package/dist/graph/memory/base.d.ts +21 -0
- package/dist/graph/memory/base.d.ts.map +1 -0
- package/dist/graph/memory/base.js +33 -0
- package/dist/graph/memory/read/envelope.d.ts +71 -0
- package/dist/graph/memory/read/envelope.d.ts.map +1 -0
- package/dist/graph/memory/read/envelope.js +29 -0
- package/dist/graph/memory/read/idle.d.ts +18 -0
- package/dist/graph/memory/read/idle.d.ts.map +1 -0
- package/dist/graph/memory/read/idle.js +16 -0
- package/dist/graph/memory/read/read.d.ts +9 -0
- package/dist/graph/memory/read/read.d.ts.map +1 -0
- package/dist/graph/memory/read/read.js +87 -0
- package/dist/graph/memory/scope.d.ts +7 -0
- package/dist/graph/memory/scope.d.ts.map +1 -0
- package/dist/graph/memory/scope.js +25 -0
- package/dist/graph/mutate/move.hook.d.ts +7 -0
- package/dist/graph/mutate/move.hook.d.ts.map +1 -0
- package/dist/graph/mutate/move.hook.js +49 -0
- package/dist/graph/mutate/mutate.hook.d.ts +17 -0
- package/dist/graph/mutate/mutate.hook.d.ts.map +1 -0
- package/dist/graph/mutate/mutate.hook.js +45 -0
- package/dist/graph/query/children.hook.d.ts +29 -0
- package/dist/graph/query/children.hook.d.ts.map +1 -0
- package/dist/graph/query/children.hook.js +64 -0
- package/dist/graph/query/graph.hook.d.ts +9 -0
- package/dist/graph/query/graph.hook.d.ts.map +1 -0
- package/dist/graph/query/graph.hook.js +5 -0
- package/dist/graph/query/neighbors.hook.d.ts +7 -0
- package/dist/graph/query/neighbors.hook.d.ts.map +1 -0
- package/dist/graph/query/neighbors.hook.js +79 -0
- package/dist/graph/query/node.hook.d.ts +14 -0
- package/dist/graph/query/node.hook.d.ts.map +1 -0
- package/dist/graph/query/node.hook.js +43 -0
- package/dist/graph/query/query.hook.d.ts +14 -0
- package/dist/graph/query/query.hook.d.ts.map +1 -0
- package/dist/graph/query/query.hook.js +23 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/intents/envelope.d.ts +33 -0
- package/dist/intents/envelope.d.ts.map +1 -0
- package/dist/intents/envelope.js +19 -0
- package/dist/intents/index.d.ts +6 -0
- package/dist/intents/index.d.ts.map +1 -0
- package/dist/intents/index.js +3 -0
- package/dist/intents/listen-intent.hook.d.ts +16 -0
- package/dist/intents/listen-intent.hook.d.ts.map +1 -0
- package/dist/intents/listen-intent.hook.js +75 -0
- package/dist/intents/reply.d.ts +9 -0
- package/dist/intents/reply.d.ts.map +1 -0
- package/dist/intents/reply.js +14 -0
- package/dist/intents/send-intent.hook.d.ts +12 -0
- package/dist/intents/send-intent.hook.d.ts.map +1 -0
- package/dist/intents/send-intent.hook.js +86 -0
- package/dist/navigation/adapters/adapter.d.ts +15 -0
- package/dist/navigation/adapters/adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/adapter.js +1 -0
- package/dist/navigation/adapters/memory.adapter.d.ts +9 -0
- package/dist/navigation/adapters/memory.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/memory.adapter.js +15 -0
- package/dist/navigation/adapters/url.adapter.d.ts +6 -0
- package/dist/navigation/adapters/url.adapter.d.ts.map +1 -0
- package/dist/navigation/adapters/url.adapter.js +59 -0
- package/dist/navigation/history.d.ts +63 -0
- package/dist/navigation/history.d.ts.map +1 -0
- package/dist/navigation/history.js +144 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts +13 -0
- package/dist/navigation/hooks/ancestors.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/ancestors.hook.js +56 -0
- package/dist/navigation/hooks/location.hook.d.ts +4 -0
- package/dist/navigation/hooks/location.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/location.hook.js +17 -0
- package/dist/navigation/hooks/navigate.hook.d.ts +12 -0
- package/dist/navigation/hooks/navigate.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/navigate.hook.js +33 -0
- package/dist/navigation/hooks/scope.hook.d.ts +12 -0
- package/dist/navigation/hooks/scope.hook.d.ts.map +1 -0
- package/dist/navigation/hooks/scope.hook.js +21 -0
- package/dist/navigation/index.d.ts +16 -0
- package/dist/navigation/index.d.ts.map +1 -0
- package/dist/navigation/index.js +10 -0
- package/dist/navigation/link.d.ts +11 -0
- package/dist/navigation/link.d.ts.map +1 -0
- package/dist/navigation/link.js +62 -0
- package/dist/navigation/location.d.ts +48 -0
- package/dist/navigation/location.d.ts.map +1 -0
- package/dist/navigation/location.js +88 -0
- package/dist/navigation/scope.context.d.ts +64 -0
- package/dist/navigation/scope.context.d.ts.map +1 -0
- package/dist/navigation/scope.context.js +178 -0
- package/dist/navigation/snapshot.d.ts +22 -0
- package/dist/navigation/snapshot.d.ts.map +1 -0
- package/dist/navigation/snapshot.js +65 -0
- package/dist/schema/bind-read.d.ts +37 -0
- package/dist/schema/bind-read.d.ts.map +1 -0
- package/dist/schema/bind-read.js +205 -0
- package/dist/schema/binding.d.ts +21 -0
- package/dist/schema/binding.d.ts.map +1 -0
- package/dist/schema/binding.js +55 -0
- package/dist/schema/domain.hook.d.ts +10 -0
- package/dist/schema/domain.hook.d.ts.map +1 -0
- package/dist/schema/domain.hook.js +7 -0
- package/dist/schema/function.hook.d.ts +9 -0
- package/dist/schema/function.hook.d.ts.map +1 -0
- package/dist/schema/function.hook.js +40 -0
- package/dist/schema/index.d.ts +10 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/kernel.hook.d.ts +10 -0
- package/dist/schema/kernel.hook.d.ts.map +1 -0
- package/dist/schema/kernel.hook.js +8 -0
- package/dist/schema/method.hook.d.ts +14 -0
- package/dist/schema/method.hook.d.ts.map +1 -0
- package/dist/schema/method.hook.js +49 -0
- package/dist/schema/tag.d.ts +11 -0
- package/dist/schema/tag.d.ts.map +1 -0
- package/dist/schema/tag.js +11 -0
- package/dist/schema/types.d.ts +73 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +1 -0
- package/dist/schema/write.d.ts +15 -0
- package/dist/schema/write.d.ts.map +1 -0
- package/dist/schema/write.js +45 -0
- package/dist/selection/index.d.ts +4 -0
- package/dist/selection/index.d.ts.map +1 -0
- package/dist/selection/index.js +4 -0
- package/dist/selection/live.d.ts +21 -0
- package/dist/selection/live.d.ts.map +1 -0
- package/dist/selection/live.js +116 -0
- package/dist/selection/scope.context.d.ts +26 -0
- package/dist/selection/scope.context.d.ts.map +1 -0
- package/dist/selection/scope.context.js +100 -0
- package/dist/selection/selection.hook.d.ts +4 -0
- package/dist/selection/selection.hook.d.ts.map +1 -0
- package/dist/selection/selection.hook.js +14 -0
- package/dist/session/boot/boot.d.ts +19 -0
- package/dist/session/boot/boot.d.ts.map +1 -0
- package/dist/session/boot/boot.js +45 -0
- package/dist/session/boot/ownership.d.ts +18 -0
- package/dist/session/boot/ownership.d.ts.map +1 -0
- package/dist/session/boot/ownership.js +39 -0
- package/dist/session/boot/status.d.ts +24 -0
- package/dist/session/boot/status.d.ts.map +1 -0
- package/dist/session/boot/status.js +11 -0
- package/dist/session/connection/connection.hook.d.ts +3 -0
- package/dist/session/connection/connection.hook.d.ts.map +1 -0
- package/dist/session/connection/connection.hook.js +12 -0
- package/dist/session/connection/connection.store.d.ts +24 -0
- package/dist/session/connection/connection.store.d.ts.map +1 -0
- package/dist/session/connection/connection.store.js +31 -0
- package/dist/session/index.d.ts +8 -0
- package/dist/session/index.d.ts.map +1 -0
- package/dist/session/index.js +6 -0
- package/dist/session/kernel-client.hook.d.ts +9 -0
- package/dist/session/kernel-client.hook.d.ts.map +1 -0
- package/dist/session/kernel-client.hook.js +5 -0
- package/dist/session/kernel-instance.hook.d.ts +9 -0
- package/dist/session/kernel-instance.hook.d.ts.map +1 -0
- package/dist/session/kernel-instance.hook.js +10 -0
- package/dist/session/provider.d.ts +31 -0
- package/dist/session/provider.d.ts.map +1 -0
- package/dist/session/provider.js +165 -0
- package/dist/session/session.context.d.ts +18 -0
- package/dist/session/session.context.d.ts.map +1 -0
- package/dist/session/session.context.js +8 -0
- package/dist/session/shell.hook.d.ts +4 -0
- package/dist/session/shell.hook.d.ts.map +1 -0
- package/dist/session/shell.hook.js +15 -0
- package/dist/window/adopt.d.ts +27 -0
- package/dist/window/adopt.d.ts.map +1 -0
- package/dist/window/adopt.js +27 -0
- package/dist/window/hooks/close-guard.hook.d.ts +6 -0
- package/dist/window/hooks/close-guard.hook.d.ts.map +1 -0
- package/dist/window/hooks/close-guard.hook.js +90 -0
- package/dist/window/hooks/target-node.hook.d.ts +7 -0
- package/dist/window/hooks/target-node.hook.d.ts.map +1 -0
- package/dist/window/hooks/target-node.hook.js +77 -0
- package/dist/window/hooks/views-for.hook.d.ts +8 -0
- package/dist/window/hooks/views-for.hook.d.ts.map +1 -0
- package/dist/window/hooks/views-for.hook.js +51 -0
- package/dist/window/hooks/window.hook.d.ts +4 -0
- package/dist/window/hooks/window.hook.d.ts.map +1 -0
- package/dist/window/hooks/window.hook.js +14 -0
- package/dist/window/hooks/windows.hook.d.ts +15 -0
- package/dist/window/hooks/windows.hook.d.ts.map +1 -0
- package/dist/window/hooks/windows.hook.js +25 -0
- package/dist/window/host.d.ts +31 -0
- package/dist/window/host.d.ts.map +1 -0
- package/dist/window/host.js +270 -0
- package/dist/window/hub.d.ts +41 -0
- package/dist/window/hub.d.ts.map +1 -0
- package/dist/window/hub.js +100 -0
- package/dist/window/index.d.ts +15 -0
- package/dist/window/index.d.ts.map +1 -0
- package/dist/window/index.js +10 -0
- package/dist/window/lifecycle/mount.d.ts +47 -0
- package/dist/window/lifecycle/mount.d.ts.map +1 -0
- package/dist/window/lifecycle/mount.js +92 -0
- package/dist/window/lifecycle/states.d.ts +33 -0
- package/dist/window/lifecycle/states.d.ts.map +1 -0
- package/dist/window/lifecycle/states.js +7 -0
- package/dist/window/placement.d.ts +33 -0
- package/dist/window/placement.d.ts.map +1 -0
- package/dist/window/placement.js +57 -0
- package/dist/window/stage.d.ts +3 -0
- package/dist/window/stage.d.ts.map +1 -0
- package/dist/window/stage.js +41 -0
- package/package.json +59 -0
- package/src/application/apps/app-host.tsx +113 -0
- package/src/application/apps/app-logo.tsx +56 -0
- package/src/application/apps/apps.hook.ts +81 -0
- package/src/application/apps/open-app.hook.ts +18 -0
- package/src/application/index.ts +22 -0
- package/src/application/roles.hook.ts +41 -0
- package/src/application/spaces/current-space.hook.ts +43 -0
- package/src/application/spaces/current-space.store.ts +110 -0
- package/src/application/spaces/space.hook.ts +41 -0
- package/src/application/spaces/spaces.hook.ts +113 -0
- package/src/application/user/user.hook.ts +27 -0
- package/src/application/user/user.store.ts +85 -0
- package/src/auth/act-as.context.tsx +124 -0
- package/src/auth/auth.hook.ts +36 -0
- package/src/auth/can.hook.ts +149 -0
- package/src/auth/index.ts +6 -0
- package/src/auth/self.hook.ts +14 -0
- package/src/auth/session-kernel.ts +24 -0
- package/src/graph/action.ts +127 -0
- package/src/graph/async.ts +87 -0
- package/src/graph/call/call.hook.ts +42 -0
- package/src/graph/call/stream.hook.ts +200 -0
- package/src/graph/graph.context.ts +14 -0
- package/src/graph/guards.error.ts +58 -0
- package/src/graph/index.ts +20 -0
- package/src/graph/memory/base.ts +50 -0
- package/src/graph/memory/read/envelope.ts +98 -0
- package/src/graph/memory/read/idle.ts +24 -0
- package/src/graph/memory/read/read.ts +103 -0
- package/src/graph/memory/scope.ts +29 -0
- package/src/graph/mutate/move.hook.ts +70 -0
- package/src/graph/mutate/mutate.hook.ts +69 -0
- package/src/graph/query/children.hook.ts +136 -0
- package/src/graph/query/graph.hook.ts +13 -0
- package/src/graph/query/neighbors.hook.ts +124 -0
- package/src/graph/query/node.hook.ts +83 -0
- package/src/graph/query/query.hook.ts +42 -0
- package/src/index.ts +202 -0
- package/src/intents/envelope.ts +52 -0
- package/src/intents/index.ts +6 -0
- package/src/intents/listen-intent.hook.ts +92 -0
- package/src/intents/reply.ts +24 -0
- package/src/intents/send-intent.hook.ts +111 -0
- package/src/navigation/adapters/adapter.ts +13 -0
- package/src/navigation/adapters/memory.adapter.ts +22 -0
- package/src/navigation/adapters/url.adapter.ts +61 -0
- package/src/navigation/history.ts +170 -0
- package/src/navigation/hooks/ancestors.hook.ts +71 -0
- package/src/navigation/hooks/location.hook.ts +21 -0
- package/src/navigation/hooks/navigate.hook.ts +43 -0
- package/src/navigation/hooks/scope.hook.ts +34 -0
- package/src/navigation/index.ts +17 -0
- package/src/navigation/link.tsx +110 -0
- package/src/navigation/location.ts +122 -0
- package/src/navigation/scope.context.tsx +267 -0
- package/src/navigation/snapshot.ts +79 -0
- package/src/schema/bind-read.ts +319 -0
- package/src/schema/binding.ts +91 -0
- package/src/schema/domain.hook.ts +16 -0
- package/src/schema/function.hook.ts +68 -0
- package/src/schema/index.ts +25 -0
- package/src/schema/kernel.hook.ts +17 -0
- package/src/schema/method.hook.ts +103 -0
- package/src/schema/tag.ts +19 -0
- package/src/schema/types.ts +150 -0
- package/src/schema/write.ts +80 -0
- package/src/selection/index.ts +5 -0
- package/src/selection/live.ts +142 -0
- package/src/selection/scope.context.tsx +151 -0
- package/src/selection/selection.hook.ts +18 -0
- package/src/session/boot/boot.ts +57 -0
- package/src/session/boot/ownership.ts +54 -0
- package/src/session/boot/status.ts +32 -0
- package/src/session/connection/connection.hook.ts +16 -0
- package/src/session/connection/connection.store.ts +58 -0
- package/src/session/index.ts +8 -0
- package/src/session/kernel-client.hook.ts +13 -0
- package/src/session/kernel-instance.hook.ts +22 -0
- package/src/session/provider.tsx +205 -0
- package/src/session/session.context.ts +27 -0
- package/src/session/shell.hook.ts +19 -0
- package/src/window/adopt.ts +57 -0
- package/src/window/hooks/close-guard.hook.ts +100 -0
- package/src/window/hooks/target-node.hook.ts +94 -0
- package/src/window/hooks/views-for.hook.ts +82 -0
- package/src/window/hooks/window.hook.ts +18 -0
- package/src/window/hooks/windows.hook.ts +40 -0
- package/src/window/host.tsx +347 -0
- package/src/window/hub.ts +156 -0
- package/src/window/index.ts +17 -0
- package/src/window/lifecycle/mount.ts +134 -0
- package/src/window/lifecycle/states.ts +23 -0
- package/src/window/placement.ts +84 -0
- package/src/window/stage.tsx +46 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BootStatus union + transitions (API §2). The provider's boot state machine:
|
|
3
|
+
* `connecting → ready` (with the resolved runtime) or `connecting → error`. The
|
|
4
|
+
* reducer is the single transition table children render behind.
|
|
5
|
+
*/
|
|
6
|
+
import type { ShellRuntime } from '../session.context'
|
|
7
|
+
|
|
8
|
+
export type BootStatus = 'connecting' | 'ready' | 'error'
|
|
9
|
+
|
|
10
|
+
export interface BootState {
|
|
11
|
+
readonly status: BootStatus
|
|
12
|
+
readonly error: unknown
|
|
13
|
+
readonly runtime: ShellRuntime | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BootAction =
|
|
17
|
+
| { readonly type: 'connecting' }
|
|
18
|
+
| { readonly type: 'ready'; readonly runtime: ShellRuntime }
|
|
19
|
+
| { readonly type: 'error'; readonly error: unknown }
|
|
20
|
+
|
|
21
|
+
export const INITIAL_BOOT: BootState = { status: 'connecting', error: undefined, runtime: null }
|
|
22
|
+
|
|
23
|
+
export function bootReducer(state: BootState, action: BootAction): BootState {
|
|
24
|
+
switch (action.type) {
|
|
25
|
+
case 'connecting':
|
|
26
|
+
return state.status === 'connecting' && state.runtime === null ? state : INITIAL_BOOT
|
|
27
|
+
case 'ready':
|
|
28
|
+
return { status: 'ready', error: undefined, runtime: action.runtime }
|
|
29
|
+
case 'error':
|
|
30
|
+
return { status: 'error', error: action.error, runtime: null }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useConnection (API §3) — transport status + memory journal position + retry.
|
|
3
|
+
* Reads the provider-owned connection store for the subtree's Shell via
|
|
4
|
+
* `useSyncExternalStore` (rule 3). Throws outside a `<ShellProvider>`.
|
|
5
|
+
*/
|
|
6
|
+
import { useSyncExternalStore } from 'react'
|
|
7
|
+
|
|
8
|
+
import type { ConnectionValue } from './connection.store'
|
|
9
|
+
|
|
10
|
+
import { useShell } from '../shell.hook'
|
|
11
|
+
import { connectionStore } from './connection.store'
|
|
12
|
+
|
|
13
|
+
export function useConnection(): ConnectionValue {
|
|
14
|
+
const store = connectionStore(useShell())
|
|
15
|
+
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot)
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connection store (API §3, rule 3). ONE per Shell — the provider owns it and
|
|
3
|
+
* registers its `retry`; `useConnection` reads it via `useSyncExternalStore` so
|
|
4
|
+
* connection churn re-renders only its subscribers. `status`/`seq` are a pinned
|
|
5
|
+
* stub ('ready'/0) until the transport-status + result-`seq` feed lands (G6).
|
|
6
|
+
*/
|
|
7
|
+
import type { Shell } from '@astrale-os/shell'
|
|
8
|
+
|
|
9
|
+
export type ConnectionStatus = 'connecting' | 'ready' | 'reconnecting' | 'error'
|
|
10
|
+
|
|
11
|
+
/** Transport + memory-journal status for the subtree (API §3). */
|
|
12
|
+
export interface ConnectionValue {
|
|
13
|
+
readonly status: ConnectionStatus
|
|
14
|
+
readonly error: unknown
|
|
15
|
+
readonly seq: number
|
|
16
|
+
retry(): void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ConnectionStore {
|
|
20
|
+
subscribe(cb: () => void): () => void
|
|
21
|
+
getSnapshot(): ConnectionValue
|
|
22
|
+
/** The provider registers its boot-retry here; a change re-stamps the snapshot. */
|
|
23
|
+
setRetry(retry: () => void): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const NOOP_RETRY = (): void => {}
|
|
27
|
+
|
|
28
|
+
function createConnectionStore(): ConnectionStore {
|
|
29
|
+
const listeners = new Set<() => void>()
|
|
30
|
+
let retry: () => void = NOOP_RETRY
|
|
31
|
+
let snapshot: ConnectionValue = { status: 'ready', error: undefined, seq: 0, retry }
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
subscribe(cb) {
|
|
35
|
+
listeners.add(cb)
|
|
36
|
+
return () => listeners.delete(cb)
|
|
37
|
+
},
|
|
38
|
+
getSnapshot: () => snapshot,
|
|
39
|
+
setRetry(next) {
|
|
40
|
+
if (next === retry) return
|
|
41
|
+
retry = next
|
|
42
|
+
snapshot = { status: snapshot.status, error: snapshot.error, seq: snapshot.seq, retry }
|
|
43
|
+
for (const l of listeners) l()
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const stores = new WeakMap<Shell, ConnectionStore>()
|
|
49
|
+
|
|
50
|
+
/** The one connection store for a Shell (created on first touch). */
|
|
51
|
+
export function connectionStore(shell: Shell): ConnectionStore {
|
|
52
|
+
let s = stores.get(shell)
|
|
53
|
+
if (s === undefined) {
|
|
54
|
+
s = createConnectionStore()
|
|
55
|
+
stores.set(shell, s)
|
|
56
|
+
}
|
|
57
|
+
return s
|
|
58
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// session — API §2 (provider) + §3 (session hooks). See SHELL_REACT_API.md.
|
|
2
|
+
export { ShellProvider } from './provider'
|
|
3
|
+
export type { ShellError, ShellProviderProps } from './provider'
|
|
4
|
+
export { useShell } from './shell.hook'
|
|
5
|
+
export { useKernelClient } from './kernel-client.hook'
|
|
6
|
+
export { useKernelInstance } from './kernel-instance.hook'
|
|
7
|
+
export type { KernelInstance } from './kernel-instance.hook'
|
|
8
|
+
export { useConnection } from './connection/connection.hook'
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useKernelClient (API §3) — `shell.kernel`, the raw bound kernel view, untouched
|
|
3
|
+
* (the untyped floor). The TYPED handles `useKernel()`/`useDomain(schema)` live in
|
|
4
|
+
* the schema module (§6).
|
|
5
|
+
*/
|
|
6
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
7
|
+
|
|
8
|
+
import { useShell } from './shell.hook'
|
|
9
|
+
|
|
10
|
+
/** `shell.kernel` — the raw bound kernel view, untouched (untyped floor). */
|
|
11
|
+
export function useKernelClient(): KernelClient {
|
|
12
|
+
return useShell().kernel
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useKernelClient } from './kernel-client.hook'
|
|
4
|
+
|
|
5
|
+
export interface KernelInstance {
|
|
6
|
+
/** Kernel envelope endpoint this shell is bound to. */
|
|
7
|
+
readonly url: string
|
|
8
|
+
/** Current kernel credential. Never cache; read per request. */
|
|
9
|
+
token(): string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Low-level access for direct kernel-envelope calls and token exchange code. */
|
|
13
|
+
export function useKernelInstance(): KernelInstance {
|
|
14
|
+
const kernel = useKernelClient()
|
|
15
|
+
return useMemo<KernelInstance>(
|
|
16
|
+
() => ({
|
|
17
|
+
url: kernel.url,
|
|
18
|
+
token: () => kernel.authToken(),
|
|
19
|
+
}),
|
|
20
|
+
[kernel],
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { MemoryEvent } from '@astrale-os/kernel-client/store'
|
|
2
|
+
import type { Shell, ShellConfig } from '@astrale-os/shell'
|
|
3
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
4
|
+
|
|
5
|
+
import { createShell } from '@astrale-os/shell'
|
|
6
|
+
/**
|
|
7
|
+
* ShellProvider — the composition root (API §2, SPEC §5): owns the shell boot
|
|
8
|
+
* state machine (a config OR a prebuilt Shell; `createShell → init → whoami →
|
|
9
|
+
* ready`), seats `ShellContext` and `GraphContext`, and registers its boot-retry
|
|
10
|
+
* into the per-shell connection store. One live shell per mounted provider,
|
|
11
|
+
* StrictMode/HMR-safe (deferred teardown), keyed on config identity. Children
|
|
12
|
+
* render only at `ready`, so useShell/useSelf are synchronous below.
|
|
13
|
+
*/
|
|
14
|
+
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
|
|
15
|
+
|
|
16
|
+
import type { GraphMemory } from '../graph/graph.context'
|
|
17
|
+
import type { BootRef } from './boot/ownership'
|
|
18
|
+
import type { SelfNode } from './session.context'
|
|
19
|
+
|
|
20
|
+
import { primeUser } from '../application/user/user.store'
|
|
21
|
+
import { GraphContext } from '../graph/graph.context'
|
|
22
|
+
import { baseMemoryFor, setMemoryErrorSink } from '../graph/memory/base'
|
|
23
|
+
import { configIdentity, isShellConfig, resolveSelf } from './boot/boot'
|
|
24
|
+
import { cancelTeardown, disposeShell, scheduleTeardown } from './boot/ownership'
|
|
25
|
+
import { bootReducer, INITIAL_BOOT } from './boot/status'
|
|
26
|
+
import { connectionStore } from './connection/connection.store'
|
|
27
|
+
import { ShellContext } from './session.context'
|
|
28
|
+
|
|
29
|
+
/** Global error channel payload (API §2). */
|
|
30
|
+
export type ShellError =
|
|
31
|
+
| { kind: 'refresh'; shape: unknown; error: unknown }
|
|
32
|
+
| { kind: 'mutation'; mutationId: string; error: unknown }
|
|
33
|
+
| { kind: 'connection'; error: unknown }
|
|
34
|
+
|
|
35
|
+
export interface ShellProviderProps {
|
|
36
|
+
/**
|
|
37
|
+
* Shell config — or a prebuilt Shell (tests, storybook). Keyed by identity: a
|
|
38
|
+
* config change disposes and reboots.
|
|
39
|
+
*/
|
|
40
|
+
config: ShellConfig | Shell
|
|
41
|
+
/** Rendered until status === 'ready'. Default: null. */
|
|
42
|
+
fallback?: ReactNode
|
|
43
|
+
/** Rendered when boot fails; `retry` re-runs init. Default: rethrow to the nearest boundary. */
|
|
44
|
+
errorFallback?: (error: unknown, retry: () => void) => ReactNode
|
|
45
|
+
/** Global error channel: warm-read refresh failures, rolled-back mutations, watch drops. */
|
|
46
|
+
onError?: (error: ShellError) => void
|
|
47
|
+
children: ReactNode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map a memory event to the documented `ShellError` union (API §2). `refresh`
|
|
52
|
+
* (warm-read revalidation failure) and `mutation` (rolled-back write) are the
|
|
53
|
+
* two the channel promises; `divergence`/`invariant` are Tier-0/dev tripwires
|
|
54
|
+
* outside the union and are not bridged here.
|
|
55
|
+
*/
|
|
56
|
+
function toShellError(event: MemoryEvent): ShellError | null {
|
|
57
|
+
switch (event.kind) {
|
|
58
|
+
case 'refresh':
|
|
59
|
+
return { kind: 'refresh', shape: event.shape, error: event.error }
|
|
60
|
+
case 'mutation':
|
|
61
|
+
return { kind: 'mutation', mutationId: event.mutationId, error: event.error }
|
|
62
|
+
default:
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ShellProvider(props: ShellProviderProps): ReactElement | null {
|
|
68
|
+
const { config, fallback, errorFallback, onError, children } = props
|
|
69
|
+
|
|
70
|
+
const [state, dispatch] = useReducer(bootReducer, INITIAL_BOOT)
|
|
71
|
+
const [attempt, setAttempt] = useState(0)
|
|
72
|
+
|
|
73
|
+
const bootRef = useRef<BootRef>({ key: null, shell: null, generation: 0 })
|
|
74
|
+
const configRef = useRef(config)
|
|
75
|
+
configRef.current = config
|
|
76
|
+
const onErrorRef = useRef(onError)
|
|
77
|
+
onErrorRef.current = onError
|
|
78
|
+
|
|
79
|
+
const configKey = configIdentity(config)
|
|
80
|
+
|
|
81
|
+
const startBoot = useCallback((ref: BootRef) => {
|
|
82
|
+
const generation = (ref.generation += 1)
|
|
83
|
+
dispatch({ type: 'connecting' })
|
|
84
|
+
|
|
85
|
+
let shell: Shell
|
|
86
|
+
const current = configRef.current
|
|
87
|
+
try {
|
|
88
|
+
shell = isShellConfig(current) ? createShell(current) : current
|
|
89
|
+
} catch (err) {
|
|
90
|
+
dispatch({ type: 'error', error: err })
|
|
91
|
+
onErrorRef.current?.({ kind: 'connection', error: err })
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
ref.shell = shell
|
|
95
|
+
|
|
96
|
+
void (async () => {
|
|
97
|
+
try {
|
|
98
|
+
await shell.init()
|
|
99
|
+
if (generation !== ref.generation) return
|
|
100
|
+
const self: SelfNode | null = await resolveSelf(shell)
|
|
101
|
+
if (generation !== ref.generation) return
|
|
102
|
+
// Pre-resolve the OS-plane user binding (API §2/§12.2) — NON-FATAL: a
|
|
103
|
+
// missing OS plane marks it unavailable without failing boot (whoami above
|
|
104
|
+
// stays the fatal gate). Seeds the user store so `useUser` is synchronous.
|
|
105
|
+
await primeUser(shell)
|
|
106
|
+
if (generation !== ref.generation) return
|
|
107
|
+
dispatch({ type: 'ready', runtime: { shell, self } })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (generation !== ref.generation) return
|
|
110
|
+
dispatch({ type: 'error', error: err })
|
|
111
|
+
onErrorRef.current?.({ kind: 'connection', error: err })
|
|
112
|
+
}
|
|
113
|
+
})()
|
|
114
|
+
}, [])
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const ref = bootRef.current
|
|
118
|
+
// The shell this provider will run with once the effect settles: a prebuilt
|
|
119
|
+
// Shell is the config object itself; a config-built one is whatever this
|
|
120
|
+
// instance already booted (adopted on a StrictMode/HMR re-setup). Cancel any
|
|
121
|
+
// pending deferred teardown for it before deciding — this also cancels a
|
|
122
|
+
// teardown another provider scheduled for the SAME prebuilt Shell.
|
|
123
|
+
const current = configRef.current
|
|
124
|
+
const adopting = isShellConfig(current) ? ref.shell : current
|
|
125
|
+
if (adopting !== null) cancelTeardown(adopting)
|
|
126
|
+
|
|
127
|
+
if (ref.key !== configKey) {
|
|
128
|
+
// First boot, config change, or a retry that cleared the key: reboot fresh.
|
|
129
|
+
if (ref.shell !== null) {
|
|
130
|
+
cancelTeardown(ref.shell)
|
|
131
|
+
disposeShell(ref.shell)
|
|
132
|
+
}
|
|
133
|
+
ref.key = configKey
|
|
134
|
+
ref.shell = null
|
|
135
|
+
startBoot(ref)
|
|
136
|
+
}
|
|
137
|
+
// Same key already booting/booted → adopt (StrictMode's second setup).
|
|
138
|
+
|
|
139
|
+
return () => {
|
|
140
|
+
// Defer teardown one macrotask; a synchronous re-setup (StrictMode/HMR or a
|
|
141
|
+
// different provider adopting the same prebuilt Shell) cancels it first and
|
|
142
|
+
// keeps the same live shell.
|
|
143
|
+
if (ref.shell !== null) scheduleTeardown(ref.shell, ref)
|
|
144
|
+
else ref.key = null
|
|
145
|
+
}
|
|
146
|
+
}, [configKey, attempt, startBoot])
|
|
147
|
+
|
|
148
|
+
const retry = useCallback(() => {
|
|
149
|
+
const ref = bootRef.current
|
|
150
|
+
ref.generation += 1
|
|
151
|
+
if (ref.shell !== null) {
|
|
152
|
+
disposeShell(ref.shell)
|
|
153
|
+
ref.shell = null
|
|
154
|
+
}
|
|
155
|
+
ref.key = null
|
|
156
|
+
dispatch({ type: 'connecting' })
|
|
157
|
+
setAttempt((a) => a + 1)
|
|
158
|
+
}, [])
|
|
159
|
+
|
|
160
|
+
// The base (default-scope) memory for the booted Shell — the SAME instance
|
|
161
|
+
// the graph/schema/nav hooks read through `graph/memory/scope.ts`. Seated in
|
|
162
|
+
// `GraphContext` so selection's liveness has a source; null until ready.
|
|
163
|
+
const shell = state.runtime?.shell ?? null
|
|
164
|
+
const memory = useMemo<GraphMemory | null>(
|
|
165
|
+
() => (shell === null ? null : baseMemoryFor(shell)),
|
|
166
|
+
[shell],
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
// Bridge the memory's event channel onto the provider `onError` (API §2/§8):
|
|
170
|
+
// warm-refresh failures and rolled-back mutations surface here rather than being
|
|
171
|
+
// swallowed. Registered while the booted shell is live; cleared on teardown.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (shell === null) return
|
|
174
|
+
setMemoryErrorSink(shell, (event) => {
|
|
175
|
+
const mapped = toShellError(event)
|
|
176
|
+
if (mapped !== null) onErrorRef.current?.(mapped)
|
|
177
|
+
})
|
|
178
|
+
return () => setMemoryErrorSink(shell, null)
|
|
179
|
+
}, [shell])
|
|
180
|
+
|
|
181
|
+
// Register this provider's boot-retry into the shell's connection store (rule 3),
|
|
182
|
+
// so `useConnection().retry` re-runs boot. Status/seq stay a stub ('ready'/0)
|
|
183
|
+
// until the transport-status + result-`seq` feed lands, with no surface change.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (shell === null) return
|
|
186
|
+
connectionStore(shell).setRetry(retry)
|
|
187
|
+
}, [shell, retry])
|
|
188
|
+
|
|
189
|
+
if (state.status === 'error') {
|
|
190
|
+
// Wrapped in a fragment so the component's declared return type stays
|
|
191
|
+
// `ReactElement | null` — valid JSX under @types/react 18 AND 19 (the react
|
|
192
|
+
// peer range is >=18), even though `errorFallback`/`fallback` yield ReactNode.
|
|
193
|
+
if (errorFallback !== undefined) return <>{errorFallback(state.error, retry)}</>
|
|
194
|
+
throw state.error
|
|
195
|
+
}
|
|
196
|
+
if (state.status !== 'ready' || state.runtime === null) {
|
|
197
|
+
return fallback !== undefined ? <>{fallback}</> : null
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<ShellContext.Provider value={state.runtime}>
|
|
202
|
+
<GraphContext.Provider value={memory}>{children}</GraphContext.Provider>
|
|
203
|
+
</ShellContext.Provider>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { KernelClient, Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ShellContext — the shell session seat. Holds ONLY the stable `Shell` for a
|
|
5
|
+
* provider subtree (plus the whoami resolved once at boot). Connection state is
|
|
6
|
+
* NOT here: it is a provider-owned store read via `useConnection` (rule 3).
|
|
7
|
+
*/
|
|
8
|
+
import { createContext } from 'react'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* whoami's node type, derived from the auth surface so shell-react needs no
|
|
12
|
+
* direct `@astrale-os/kernel-core` dependency (`Node` is not yet re-exported by
|
|
13
|
+
* kernel-client — API §14 G8). Swap for the blessed re-export when it lands.
|
|
14
|
+
*/
|
|
15
|
+
export type SelfNode = Awaited<ReturnType<KernelClient['auth']['whoami']>>
|
|
16
|
+
|
|
17
|
+
/** The live shell for a provider subtree (and, later, an `<ActAs>` subtree). */
|
|
18
|
+
export interface ShellRuntime {
|
|
19
|
+
readonly shell: Shell
|
|
20
|
+
/**
|
|
21
|
+
* whoami resolved at boot; the auth module (`useSelf`) reads it. Null when the
|
|
22
|
+
* bound kernel exposes no auth surface (a bare prebuilt Shell in tests).
|
|
23
|
+
*/
|
|
24
|
+
readonly self: SelfNode | null
|
|
25
|
+
}
|
|
26
|
+
export const ShellContext = createContext<ShellRuntime | null>(null)
|
|
27
|
+
ShellContext.displayName = 'AstraleShellContext'
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useShell (API §3) — the live shell for this subtree, read from ShellContext.
|
|
5
|
+
* Throws outside a `<ShellProvider>`. The imperative floor every other session
|
|
6
|
+
* hook builds on.
|
|
7
|
+
*/
|
|
8
|
+
import { useContext } from 'react'
|
|
9
|
+
|
|
10
|
+
import { ShellContext } from './session.context'
|
|
11
|
+
|
|
12
|
+
/** The live shell for this subtree. Throws outside a `<ShellProvider>`. */
|
|
13
|
+
export function useShell(): Shell {
|
|
14
|
+
const runtime = useContext(ShellContext)
|
|
15
|
+
if (runtime === null) {
|
|
16
|
+
throw new Error('useShell must be used within a <ShellProvider>')
|
|
17
|
+
}
|
|
18
|
+
return runtime.shell
|
|
19
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stash/adopt across re-key / re-parent (API §10). A live window handed off across
|
|
3
|
+
* an unmount so a remount can ADOPT it instead of closing + reloading it. Mirrors
|
|
4
|
+
* ShellProvider's deferred-teardown pattern: the unmounting host stashes the
|
|
5
|
+
* record, and either the remount takes it (one macrotask) or it disposes.
|
|
6
|
+
*/
|
|
7
|
+
import type { Window } from '@astrale-os/shell'
|
|
8
|
+
|
|
9
|
+
import type { WindowState } from './lifecycle/states'
|
|
10
|
+
import type { StageSlot } from './placement'
|
|
11
|
+
|
|
12
|
+
export interface AdoptRecord {
|
|
13
|
+
readonly windowId: string
|
|
14
|
+
readonly window: Window
|
|
15
|
+
readonly slot: StageSlot
|
|
16
|
+
readonly state: WindowState
|
|
17
|
+
readonly targetId: string | undefined
|
|
18
|
+
/** Close the window, release its slot, drop it from the registry (fired if unadopted). */
|
|
19
|
+
dispose(): void
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AdoptStash {
|
|
23
|
+
/** Stash an unmounting host's live window; disposes after one macrotask if unadopted. */
|
|
24
|
+
stash(key: string, record: AdoptRecord): void
|
|
25
|
+
/** Reclaim a stashed window for a remount of the same source, cancelling its dispose. */
|
|
26
|
+
take(key: string): AdoptRecord | null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The pending-adoption registry — one per stage hub. */
|
|
30
|
+
export function createAdoptStash(): AdoptStash {
|
|
31
|
+
const pendingAdopt = new Map<
|
|
32
|
+
string,
|
|
33
|
+
{ record: AdoptRecord; timer: ReturnType<typeof setTimeout> }
|
|
34
|
+
>()
|
|
35
|
+
return {
|
|
36
|
+
stash(key, record) {
|
|
37
|
+
// Rapid same-key churn: an unclaimed prior stash disposes now.
|
|
38
|
+
const prior = pendingAdopt.get(key)
|
|
39
|
+
if (prior !== undefined) {
|
|
40
|
+
clearTimeout(prior.timer)
|
|
41
|
+
prior.record.dispose()
|
|
42
|
+
}
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
pendingAdopt.delete(key)
|
|
45
|
+
record.dispose()
|
|
46
|
+
}, 0)
|
|
47
|
+
pendingAdopt.set(key, { record, timer })
|
|
48
|
+
},
|
|
49
|
+
take(key) {
|
|
50
|
+
const entry = pendingAdopt.get(key)
|
|
51
|
+
if (entry === undefined) return null
|
|
52
|
+
clearTimeout(entry.timer)
|
|
53
|
+
pendingAdopt.delete(key)
|
|
54
|
+
return entry.record
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Shell } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useCloseGuard (API §10) — child-side willClose negotiation. The parent sends a
|
|
5
|
+
* `willClose` intent before closing; the guard answers `closeAck` (allow) or
|
|
6
|
+
* `closeRefuse` (veto, with reason). Guards stack per shell and the last-mounted
|
|
7
|
+
* one wins; a single shared subscription owns the reply so a `willClose` is
|
|
8
|
+
* answered exactly once regardless of how many guards are mounted.
|
|
9
|
+
*/
|
|
10
|
+
import { useEffect, useRef } from 'react'
|
|
11
|
+
|
|
12
|
+
import { useShell } from '../../session/shell.hook'
|
|
13
|
+
|
|
14
|
+
export type CloseVerdict = 'ack' | 'refuse' | { readonly refuse: string }
|
|
15
|
+
export type CloseGuard = () => CloseVerdict | Promise<CloseVerdict>
|
|
16
|
+
|
|
17
|
+
interface CloseGuardHub {
|
|
18
|
+
push(guard: CloseGuard): () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sendAck(parent: NonNullable<Shell['parent']>): void {
|
|
22
|
+
parent.send({
|
|
23
|
+
type: 'intent',
|
|
24
|
+
version: 1,
|
|
25
|
+
envelope: { name: 'closeAck', payload: {}, sender: { windowId: 'self' } },
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sendRefuse(parent: NonNullable<Shell['parent']>, reason: string): void {
|
|
30
|
+
parent.send({
|
|
31
|
+
type: 'intent',
|
|
32
|
+
version: 1,
|
|
33
|
+
envelope: { name: 'closeRefuse', payload: { reason }, sender: { windowId: 'self' } },
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const HUBS = new WeakMap<Shell, CloseGuardHub>()
|
|
38
|
+
|
|
39
|
+
function getCloseGuardHub(shell: Shell): CloseGuardHub {
|
|
40
|
+
const existing = HUBS.get(shell)
|
|
41
|
+
if (existing !== undefined) return existing
|
|
42
|
+
|
|
43
|
+
const stack: CloseGuard[] = []
|
|
44
|
+
let off: (() => void) | null = null
|
|
45
|
+
|
|
46
|
+
function ensureSubscribed(): void {
|
|
47
|
+
if (off !== null) return
|
|
48
|
+
const parent = shell.parent
|
|
49
|
+
if (parent === null) return
|
|
50
|
+
off = parent.on('intent', (message) => {
|
|
51
|
+
if (message.envelope.name !== 'willClose') return
|
|
52
|
+
const guard = stack[stack.length - 1]
|
|
53
|
+
if (guard === undefined) return
|
|
54
|
+
void answer(parent, guard)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function answer(parent: NonNullable<Shell['parent']>, guard: CloseGuard): Promise<void> {
|
|
59
|
+
let verdict: CloseVerdict
|
|
60
|
+
try {
|
|
61
|
+
verdict = await guard()
|
|
62
|
+
} catch {
|
|
63
|
+
// A throwing guard must not trap the user in the window — allow the close.
|
|
64
|
+
verdict = 'ack'
|
|
65
|
+
}
|
|
66
|
+
if (verdict === 'ack') sendAck(parent)
|
|
67
|
+
else if (verdict === 'refuse') sendRefuse(parent, '')
|
|
68
|
+
else sendRefuse(parent, verdict.refuse)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const hub: CloseGuardHub = {
|
|
72
|
+
push(guard) {
|
|
73
|
+
stack.push(guard)
|
|
74
|
+
ensureSubscribed()
|
|
75
|
+
return () => {
|
|
76
|
+
const i = stack.lastIndexOf(guard)
|
|
77
|
+
if (i >= 0) stack.splice(i, 1)
|
|
78
|
+
if (stack.length === 0 && off !== null) {
|
|
79
|
+
off()
|
|
80
|
+
off = null
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
}
|
|
85
|
+
HUBS.set(shell, hub)
|
|
86
|
+
return hub
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useCloseGuard(guard: CloseGuard): void {
|
|
90
|
+
const shell = useShell()
|
|
91
|
+
const guardRef = useRef(guard)
|
|
92
|
+
guardRef.current = guard
|
|
93
|
+
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
const hub = getCloseGuardHub(shell)
|
|
96
|
+
// Push a stable wrapper so a changed guard closure never re-registers
|
|
97
|
+
// (latest-handler semantics; unmount pops this exact entry).
|
|
98
|
+
return hub.push(() => guardRef.current())
|
|
99
|
+
}, [shell])
|
|
100
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { KernelClient } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useTargetNode (API §10) — inside a sandboxed view, the node it was opened on.
|
|
5
|
+
* The id starts from `shell.targetNodeId` (handshake) and a host `setTarget`
|
|
6
|
+
* intent retargets it; the id resolves to a live `Node` through the shell kernel.
|
|
7
|
+
*
|
|
8
|
+
* Honest subset (flagged): the read is a one-shot `kernel.get` re-run on id
|
|
9
|
+
* change, not a memory watch, so `live` is false. Once composed with `useNode(id)`
|
|
10
|
+
* it becomes real stale-while-revalidate liveness with no consumer change.
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
import type { ReadState } from '../../graph/memory/read/envelope'
|
|
15
|
+
import type { Node } from '../lifecycle/mount'
|
|
16
|
+
|
|
17
|
+
import { idleReadState } from '../../graph/memory/read/idle'
|
|
18
|
+
import { useShell } from '../../session/shell.hook'
|
|
19
|
+
|
|
20
|
+
export interface NodeResult extends ReadState {
|
|
21
|
+
readonly node: Node | null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Address a bare node id by graph id (`@<id>`); pass paths/anchors through. */
|
|
25
|
+
function toRef(id: string): string {
|
|
26
|
+
return id.startsWith('/') || id.startsWith('@') ? id : `@${id}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface ReadSnapshot {
|
|
30
|
+
readonly node: Node | null
|
|
31
|
+
readonly pending: boolean
|
|
32
|
+
readonly error: unknown
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const IDLE_READ: ReadSnapshot = { node: null, pending: false, error: undefined }
|
|
36
|
+
|
|
37
|
+
export function useTargetNode(): NodeResult {
|
|
38
|
+
const shell = useShell()
|
|
39
|
+
const [id, setId] = useState<string | null>(() => shell.targetNodeId ?? null)
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const parent = shell.parent
|
|
43
|
+
if (parent === null) return
|
|
44
|
+
return parent.on('intent', (message) => {
|
|
45
|
+
if (message.envelope.name !== 'setTarget') return
|
|
46
|
+
const nodeId = (message.envelope.payload as { nodeId?: string }).nodeId
|
|
47
|
+
if (typeof nodeId === 'string') setId(nodeId)
|
|
48
|
+
})
|
|
49
|
+
}, [shell])
|
|
50
|
+
|
|
51
|
+
return useNodeRead(shell.kernel, id)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One-shot get + refetch, in the read envelope. The graph-module seam. */
|
|
55
|
+
function useNodeRead(kernel: KernelClient, id: string | null): NodeResult {
|
|
56
|
+
const [snap, setSnap] = useState<ReadSnapshot>(IDLE_READ)
|
|
57
|
+
const aliveRef = useRef(true)
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
aliveRef.current = true
|
|
60
|
+
return () => {
|
|
61
|
+
aliveRef.current = false
|
|
62
|
+
}
|
|
63
|
+
}, [])
|
|
64
|
+
|
|
65
|
+
const load = useCallback(async (): Promise<void> => {
|
|
66
|
+
if (id === null) {
|
|
67
|
+
if (aliveRef.current) setSnap(IDLE_READ)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (aliveRef.current) setSnap((s) => ({ node: s.node, pending: true, error: undefined }))
|
|
71
|
+
try {
|
|
72
|
+
const node = await kernel.get(toRef(id))
|
|
73
|
+
if (aliveRef.current) setSnap({ node: node ?? null, pending: false, error: undefined })
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (aliveRef.current) setSnap((s) => ({ node: s.node, pending: false, error }))
|
|
76
|
+
}
|
|
77
|
+
}, [kernel, id])
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
void load()
|
|
81
|
+
}, [load])
|
|
82
|
+
|
|
83
|
+
if (id === null) {
|
|
84
|
+
return { ...idleReadState(), node: null }
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
node: snap.node,
|
|
88
|
+
pending: snap.pending,
|
|
89
|
+
error: snap.error,
|
|
90
|
+
live: false,
|
|
91
|
+
seq: 0,
|
|
92
|
+
refetch: load,
|
|
93
|
+
}
|
|
94
|
+
}
|