@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,82 @@
|
|
|
1
|
+
import type { PathLike } from '@astrale-os/kernel-client/graph'
|
|
2
|
+
|
|
3
|
+
import { K } from '@astrale-os/kernel-core'
|
|
4
|
+
/**
|
|
5
|
+
* useViewsFor (API §10) — "what Views can render this node", as mountable
|
|
6
|
+
* `ViewDescriptor`s for the open-with menu. Backed one-shot (`live:false`) by the
|
|
7
|
+
* kernel `View.resolve` syscall, which derives the node's class and returns the
|
|
8
|
+
* caller-visible Views (USE-visibility enforced kernel-side), each with mount url.
|
|
9
|
+
*/
|
|
10
|
+
import { useMemo } from 'react'
|
|
11
|
+
|
|
12
|
+
import type { ReadState } from '../../graph/memory/read/envelope'
|
|
13
|
+
import type { Node, ViewDescriptor } from '../lifecycle/mount'
|
|
14
|
+
|
|
15
|
+
import { useAsyncResource } from '../../graph/async'
|
|
16
|
+
import { useKernelClient } from '../../session/kernel-client.hook'
|
|
17
|
+
|
|
18
|
+
export interface ViewsForResult extends ReadState {
|
|
19
|
+
readonly views: readonly ViewDescriptor[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The `View.resolve` wire address — the qualified static-method slug from the schema. */
|
|
23
|
+
const VIEW_RESOLVE_METHOD = K.View.resolve.path.method.raw
|
|
24
|
+
|
|
25
|
+
/** The kernel `View.resolve` row shape (id/path/url/name?/handshake?/origin). */
|
|
26
|
+
interface ViewOut {
|
|
27
|
+
readonly id: string
|
|
28
|
+
readonly path: string
|
|
29
|
+
readonly url: string
|
|
30
|
+
readonly name?: string
|
|
31
|
+
readonly handshake?: 'shell' | 'none'
|
|
32
|
+
readonly origin: 'self' | 'class'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const NO_VIEWS: readonly ViewDescriptor[] = Object.freeze([])
|
|
36
|
+
|
|
37
|
+
export function useViewsFor(node: Node | PathLike | null | undefined): ViewsForResult {
|
|
38
|
+
const kernel = useKernelClient()
|
|
39
|
+
const target = resolvePath(node)
|
|
40
|
+
|
|
41
|
+
const res = useAsyncResource<readonly ViewDescriptor[]>(
|
|
42
|
+
target === null ? null : () => resolveViews(kernel, target),
|
|
43
|
+
NO_VIEWS,
|
|
44
|
+
[kernel, target],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return useMemo<ViewsForResult>(
|
|
48
|
+
() => ({
|
|
49
|
+
views: res.data,
|
|
50
|
+
pending: res.pending,
|
|
51
|
+
error: res.error,
|
|
52
|
+
live: false,
|
|
53
|
+
seq: 0,
|
|
54
|
+
refetch: res.refetch,
|
|
55
|
+
}),
|
|
56
|
+
[res.data, res.pending, res.error, res.refetch],
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Dispatch `View.resolve` and project its rows to mountable `ViewDescriptor`s. */
|
|
61
|
+
async function resolveViews(
|
|
62
|
+
kernel: { call(method: string, params: unknown): Promise<unknown> },
|
|
63
|
+
node: string,
|
|
64
|
+
): Promise<readonly ViewDescriptor[]> {
|
|
65
|
+
const rows = (await kernel.call(VIEW_RESOLVE_METHOD, { node })) as ViewOut[] | null | undefined
|
|
66
|
+
if (rows === null || rows === undefined || rows.length === 0) return NO_VIEWS
|
|
67
|
+
return rows.map((r) => ({ id: r.id, path: r.path, url: r.url, handshake: r.handshake }))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A path/`@id` the kernel read surface accepts, or `null` for an idle subject. */
|
|
71
|
+
function resolvePath(node: Node | PathLike | null | undefined): string | null {
|
|
72
|
+
if (node === null || node === undefined) return null
|
|
73
|
+
if (typeof node === 'string') {
|
|
74
|
+
if (node.length === 0) return null
|
|
75
|
+
return node.startsWith('/') || node.startsWith('@') ? node : `@${node}`
|
|
76
|
+
}
|
|
77
|
+
const obj = node as { path?: { raw?: unknown }; id?: unknown; raw?: unknown }
|
|
78
|
+
if (typeof obj.path?.raw === 'string') return obj.path.raw
|
|
79
|
+
if (typeof obj.id === 'string') return `@${obj.id}`
|
|
80
|
+
if (typeof obj.raw === 'string') return obj.raw
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useWindow(id) (API §10) — one window's live phase + shell `Window` (null id /
|
|
3
|
+
* unknown id → a stable "no window"), reactive over the per-shell hub registry.
|
|
4
|
+
*/
|
|
5
|
+
import { useMemo, useSyncExternalStore } from 'react'
|
|
6
|
+
|
|
7
|
+
import type { WindowView } from '../hub'
|
|
8
|
+
|
|
9
|
+
import { useShell } from '../../session/shell.hook'
|
|
10
|
+
import { getStageHub } from '../hub'
|
|
11
|
+
|
|
12
|
+
/** One window's live phase + shell `Window` (null id / unknown id → no window). */
|
|
13
|
+
export function useWindow(windowId: string | null): WindowView {
|
|
14
|
+
const shell = useShell()
|
|
15
|
+
const hub = useMemo(() => getStageHub(shell), [shell])
|
|
16
|
+
const snapshot = useMemo(() => () => hub.registry.select(windowId), [hub, windowId])
|
|
17
|
+
return useSyncExternalStore(hub.registry.subscribe, snapshot, snapshot)
|
|
18
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Window } from '@astrale-os/shell'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useWindows / useWindowList (API §10) — reactive views over the windows a
|
|
5
|
+
* `ViewHost`/`AppHost` mounts through the per-shell hub. The shell exposes no
|
|
6
|
+
* window-change feed, so the hub registry (fed by the hosts) is the reactive
|
|
7
|
+
* source; windows mounted imperatively via `shell.mount` outside React are not
|
|
8
|
+
* tracked here — flagged in the module report as a shell foundation gap.
|
|
9
|
+
*/
|
|
10
|
+
import { useMemo, useSyncExternalStore } from 'react'
|
|
11
|
+
|
|
12
|
+
import { useShell } from '../../session/shell.hook'
|
|
13
|
+
import { getStageHub } from '../hub'
|
|
14
|
+
|
|
15
|
+
/** Every host-mounted window in this shell's tree, keyed by windowId. Reactive. */
|
|
16
|
+
export function useWindows(): ReadonlyMap<string, Window> {
|
|
17
|
+
const shell = useShell()
|
|
18
|
+
const hub = useMemo(() => getStageHub(shell), [shell])
|
|
19
|
+
return useSyncExternalStore(
|
|
20
|
+
hub.registry.subscribe,
|
|
21
|
+
hub.registry.getSnapshot,
|
|
22
|
+
hub.registry.getSnapshot,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One open window as a list entry. */
|
|
27
|
+
export interface WindowListEntry {
|
|
28
|
+
readonly windowId: string
|
|
29
|
+
readonly window: Window
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The open windows as a reactive, insertion-ordered list — the array form of
|
|
34
|
+
* `useWindows()`. The underlying map is referentially stable between mutations,
|
|
35
|
+
* so the derived array is too (no render churn).
|
|
36
|
+
*/
|
|
37
|
+
export function useWindowList(): readonly WindowListEntry[] {
|
|
38
|
+
const windows = useWindows()
|
|
39
|
+
return useMemo(() => [...windows].map(([windowId, window]) => ({ windowId, window })), [windows])
|
|
40
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { NodeRefInput } from '@astrale-os/kernel-client'
|
|
2
|
+
import type {
|
|
3
|
+
CloseResult,
|
|
4
|
+
IntentRegistry,
|
|
5
|
+
MountCredential,
|
|
6
|
+
MountParams,
|
|
7
|
+
SandboxProfile,
|
|
8
|
+
Window,
|
|
9
|
+
} from '@astrale-os/shell'
|
|
10
|
+
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ViewHost (API §10) — hosts one view. It renders a normal-flow anchor and
|
|
14
|
+
* projects that box onto a stage slot (ResizeObserver); the iframe lives in the
|
|
15
|
+
* stage, so reorder/re-key never reloads a healthy frame. It drives the
|
|
16
|
+
* `WindowState` machine, owns teardown (unmount closes its window), survives
|
|
17
|
+
* StrictMode double-mount, and downgrades a fallbackable handshake to plain.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
forwardRef,
|
|
21
|
+
useCallback,
|
|
22
|
+
useEffect,
|
|
23
|
+
useImperativeHandle,
|
|
24
|
+
useLayoutEffect,
|
|
25
|
+
useMemo,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from 'react'
|
|
29
|
+
|
|
30
|
+
import type { AdoptRecord } from './adopt'
|
|
31
|
+
import type { ViewSource } from './lifecycle/mount'
|
|
32
|
+
import type { WindowState } from './lifecycle/states'
|
|
33
|
+
import type { SlotRect, StageController, StageSlot } from './placement'
|
|
34
|
+
|
|
35
|
+
import { useShell } from '../session/shell.hook'
|
|
36
|
+
import { getStageHub } from './hub'
|
|
37
|
+
import {
|
|
38
|
+
isFallbackableHandshakeError,
|
|
39
|
+
refToId,
|
|
40
|
+
resolveSource,
|
|
41
|
+
sendSetTarget,
|
|
42
|
+
sourceKey,
|
|
43
|
+
} from './lifecycle/mount'
|
|
44
|
+
import { rectOf, sameRect } from './placement'
|
|
45
|
+
|
|
46
|
+
/** Imperative handle on a mounted `ViewHost` (API §10). */
|
|
47
|
+
export interface ViewHostHandle {
|
|
48
|
+
readonly windowId: string | null
|
|
49
|
+
close(opts?: { force?: boolean; timeoutMs?: number }): Promise<CloseResult>
|
|
50
|
+
/** The one explicit remount. */
|
|
51
|
+
refresh(): void
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type ViewHostProps = ViewSource & {
|
|
55
|
+
/** Node this view opens on — delivered as `targetNodeId`; a change retargets. */
|
|
56
|
+
node?: NodeRefInput
|
|
57
|
+
capabilities?: { intents: readonly (keyof IntentRegistry)[] }
|
|
58
|
+
sandbox?: SandboxProfile | null
|
|
59
|
+
credential?: MountCredential
|
|
60
|
+
/** Rendered inside the anchor while the frame is not `ready`. */
|
|
61
|
+
fallback?: (state: WindowState) => ReactNode
|
|
62
|
+
onStateChange?: (state: WindowState) => void
|
|
63
|
+
className?: string
|
|
64
|
+
style?: CSSProperties
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const MOUNTING: WindowState = { phase: 'mounting' }
|
|
68
|
+
|
|
69
|
+
/** The live window a host owns — enough to hand off (adopt) across a remount. */
|
|
70
|
+
interface LiveWindow {
|
|
71
|
+
readonly windowId: string
|
|
72
|
+
readonly window: Window
|
|
73
|
+
readonly state: WindowState
|
|
74
|
+
readonly targetId: string | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const ViewHost = forwardRef<ViewHostHandle, ViewHostProps>(
|
|
78
|
+
function ViewHost(props, ref): ReactElement {
|
|
79
|
+
const shell = useShell()
|
|
80
|
+
const hub = useMemo(() => getStageHub(shell), [shell])
|
|
81
|
+
|
|
82
|
+
const anchorRef = useRef<HTMLDivElement | null>(null)
|
|
83
|
+
const slotRef = useRef<StageSlot | null>(null)
|
|
84
|
+
const windowIdRef = useRef<string | null>(null)
|
|
85
|
+
const aliveRef = useRef(true)
|
|
86
|
+
// The live window (set at ready / on adopt, cleared on close) — the unit an
|
|
87
|
+
// unmount stashes for a re-key/re-parent remount to adopt (§10).
|
|
88
|
+
const liveRef = useRef<LiveWindow | null>(null)
|
|
89
|
+
// Set by `refresh()`: the next unmount CLOSES (real remount) instead of stashing.
|
|
90
|
+
const refreshingRef = useRef(false)
|
|
91
|
+
// Last placed slot rect — reset when the slot changes; guards redundant places.
|
|
92
|
+
const lastRectRef = useRef<SlotRect | null>(null)
|
|
93
|
+
|
|
94
|
+
const propsRef = useRef(props)
|
|
95
|
+
propsRef.current = props
|
|
96
|
+
|
|
97
|
+
const [state, setState] = useState<WindowState>(MOUNTING)
|
|
98
|
+
const [refreshNonce, setRefreshNonce] = useState(0)
|
|
99
|
+
|
|
100
|
+
const mountKey = sourceKey(props)
|
|
101
|
+
const targetId = refToId(props.node)
|
|
102
|
+
const lastTargetRef = useRef<string | undefined>(undefined)
|
|
103
|
+
|
|
104
|
+
const setStateSafe = (next: WindowState) => {
|
|
105
|
+
if (!aliveRef.current) return
|
|
106
|
+
setState(next)
|
|
107
|
+
propsRef.current.onStateChange?.(next)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
aliveRef.current = true
|
|
112
|
+
return () => {
|
|
113
|
+
aliveRef.current = false
|
|
114
|
+
}
|
|
115
|
+
}, [])
|
|
116
|
+
|
|
117
|
+
useImperativeHandle(
|
|
118
|
+
ref,
|
|
119
|
+
(): ViewHostHandle => ({
|
|
120
|
+
get windowId() {
|
|
121
|
+
return windowIdRef.current
|
|
122
|
+
},
|
|
123
|
+
async close(opts): Promise<CloseResult> {
|
|
124
|
+
const id = windowIdRef.current
|
|
125
|
+
if (id === null) return { kind: 'noop' }
|
|
126
|
+
const result = await shell.close(id, opts)
|
|
127
|
+
if (result.kind === 'closed') {
|
|
128
|
+
windowIdRef.current = null
|
|
129
|
+
liveRef.current = null
|
|
130
|
+
hub.registry.remove(id)
|
|
131
|
+
setStateSafe({ phase: 'closed', windowId: id })
|
|
132
|
+
}
|
|
133
|
+
return result
|
|
134
|
+
},
|
|
135
|
+
refresh() {
|
|
136
|
+
// A real remount: the next unmount closes (not stashes) so the frame reloads.
|
|
137
|
+
refreshingRef.current = true
|
|
138
|
+
setRefreshNonce((n) => n + 1)
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
[shell, hub],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// ── Placement: project the anchor box onto the current stage slot. Guarded by
|
|
145
|
+
// rect-equality, so it is cheap to call on every commit (a sibling reorder
|
|
146
|
+
// moves the anchor with no resize/scroll event — API §10).
|
|
147
|
+
const measure = useCallback((): void => {
|
|
148
|
+
const slot = slotRef.current
|
|
149
|
+
const anchor = anchorRef.current
|
|
150
|
+
if (slot === null || anchor === null) return
|
|
151
|
+
const rect = rectOf(anchor)
|
|
152
|
+
const last = lastRectRef.current
|
|
153
|
+
if (last !== null && sameRect(last, rect)) return
|
|
154
|
+
lastRectRef.current = rect
|
|
155
|
+
slot.place(rect)
|
|
156
|
+
}, [])
|
|
157
|
+
|
|
158
|
+
// ── Mount lifecycle. Keyed on the source identity + refresh nonce only — a
|
|
159
|
+
// node (targetNodeId) change retargets via setTarget, never a remount. A
|
|
160
|
+
// re-key/re-parent unmount+remount ADOPTS the live window through the hub
|
|
161
|
+
// (deferred close), so a healthy iframe is never reloaded (§10).
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
let cancelled = false
|
|
164
|
+
let controllerWaiter: (() => void) | null = null
|
|
165
|
+
// Consume the refresh flag: this setup is the (real) remount a refresh asked
|
|
166
|
+
// for, so it must NOT adopt the window its own cleanup just closed.
|
|
167
|
+
const wasRefresh = refreshingRef.current
|
|
168
|
+
refreshingRef.current = false
|
|
169
|
+
|
|
170
|
+
/** Close the window, release its slot, drop it from the registry (unadopted). */
|
|
171
|
+
function closeAndRelease(id: string, slot: StageSlot): void {
|
|
172
|
+
void shell
|
|
173
|
+
.close(id, { force: false })
|
|
174
|
+
.then((r) => (r.kind === 'closed' ? undefined : shell.close(id, { force: true })))
|
|
175
|
+
.catch(() => {})
|
|
176
|
+
.finally(() => hub.registry.remove(id))
|
|
177
|
+
slot.release()
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Stash the live window for adoption, or (on refresh) close it outright. */
|
|
181
|
+
function cleanup(): void {
|
|
182
|
+
cancelled = true
|
|
183
|
+
controllerWaiter?.()
|
|
184
|
+
const live = liveRef.current
|
|
185
|
+
const slot = slotRef.current
|
|
186
|
+
slotRef.current = null
|
|
187
|
+
windowIdRef.current = null
|
|
188
|
+
liveRef.current = null
|
|
189
|
+
lastRectRef.current = null
|
|
190
|
+
if (live === null || slot === null) {
|
|
191
|
+
// Mount never resolved: release the slot; an in-flight mount self-closes.
|
|
192
|
+
slot?.release()
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
const record: AdoptRecord = {
|
|
196
|
+
windowId: live.windowId,
|
|
197
|
+
window: live.window,
|
|
198
|
+
slot,
|
|
199
|
+
state: live.state,
|
|
200
|
+
targetId: live.targetId,
|
|
201
|
+
dispose: () => closeAndRelease(live.windowId, slot),
|
|
202
|
+
}
|
|
203
|
+
if (refreshingRef.current) record.dispose()
|
|
204
|
+
else hub.stashForAdopt(mountKey, record)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Adopt a live window a just-unmounted same-source host left behind.
|
|
208
|
+
if (!wasRefresh) {
|
|
209
|
+
const adopted = hub.takeAdopt(mountKey)
|
|
210
|
+
if (adopted !== null) {
|
|
211
|
+
slotRef.current = adopted.slot
|
|
212
|
+
windowIdRef.current = adopted.windowId
|
|
213
|
+
liveRef.current = {
|
|
214
|
+
windowId: adopted.windowId,
|
|
215
|
+
window: adopted.window,
|
|
216
|
+
state: adopted.state,
|
|
217
|
+
targetId: adopted.targetId,
|
|
218
|
+
}
|
|
219
|
+
lastTargetRef.current = adopted.targetId
|
|
220
|
+
hub.registry.upsert(adopted.windowId, adopted.window, adopted.state)
|
|
221
|
+
lastRectRef.current = null
|
|
222
|
+
measure() // reposition the surviving slot onto this host's anchor
|
|
223
|
+
setStateSafe(adopted.state)
|
|
224
|
+
return cleanup
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function mountWith(handshake: 'shell' | 'none', downgraded: boolean): Promise<void> {
|
|
229
|
+
const p = propsRef.current
|
|
230
|
+
let resolved
|
|
231
|
+
try {
|
|
232
|
+
resolved = await resolveSource(shell.kernel, sourceOf(p))
|
|
233
|
+
} catch (err) {
|
|
234
|
+
// No window yet (source resolution failed) — windowId honestly absent.
|
|
235
|
+
if (!cancelled) setStateSafe({ phase: 'crashed', error: err })
|
|
236
|
+
return
|
|
237
|
+
}
|
|
238
|
+
if (cancelled) return
|
|
239
|
+
|
|
240
|
+
const usedHandshake = handshake === 'none' ? 'none' : resolved.handshake
|
|
241
|
+
const deliveredTarget = refToId(p.node)
|
|
242
|
+
const params: MountParams = {
|
|
243
|
+
host: slotRef.current!.element,
|
|
244
|
+
url: resolved.url,
|
|
245
|
+
functionId: resolved.functionId,
|
|
246
|
+
capabilities: { intents: p.capabilities?.intents ?? [] },
|
|
247
|
+
targetNodeId: deliveredTarget,
|
|
248
|
+
sandbox: p.sandbox,
|
|
249
|
+
handshake: usedHandshake,
|
|
250
|
+
credential: p.credential,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const win = await shell.mount(params)
|
|
255
|
+
// A StrictMode cleanup (or refresh) fired before mount resolved: adopt
|
|
256
|
+
// then immediately close so no orphan window survives.
|
|
257
|
+
if (cancelled) {
|
|
258
|
+
void win.close({ force: true }).catch(() => {})
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
windowIdRef.current = win.windowId
|
|
262
|
+
lastTargetRef.current = deliveredTarget
|
|
263
|
+
const ready: WindowState = downgraded
|
|
264
|
+
? { phase: 'ready', windowId: win.windowId, downgraded: true }
|
|
265
|
+
: { phase: 'ready', windowId: win.windowId }
|
|
266
|
+
liveRef.current = {
|
|
267
|
+
windowId: win.windowId,
|
|
268
|
+
window: win.window,
|
|
269
|
+
state: ready,
|
|
270
|
+
targetId: deliveredTarget,
|
|
271
|
+
}
|
|
272
|
+
hub.registry.upsert(win.windowId, win.window, ready)
|
|
273
|
+
setStateSafe(ready)
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (cancelled) return
|
|
276
|
+
if (usedHandshake === 'shell' && isFallbackableHandshakeError(err)) {
|
|
277
|
+
await mountWith('none', true)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
setStateSafe({ phase: 'crashed', windowId: windowIdRef.current ?? undefined, error: err })
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function begin(controller: StageController): void {
|
|
285
|
+
if (cancelled || slotRef.current !== null) return
|
|
286
|
+
const slot = controller.acquire()
|
|
287
|
+
slotRef.current = slot
|
|
288
|
+
lastRectRef.current = null
|
|
289
|
+
measure()
|
|
290
|
+
setStateSafe(MOUNTING)
|
|
291
|
+
void mountWith('shell', false)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const existing = hub.getController()
|
|
295
|
+
if (existing !== null) begin(existing)
|
|
296
|
+
else {
|
|
297
|
+
controllerWaiter = hub.onController((c) => {
|
|
298
|
+
if (c !== null) begin(c)
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return cleanup
|
|
303
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
304
|
+
}, [hub, shell, mountKey, refreshNonce])
|
|
305
|
+
|
|
306
|
+
// ── Retarget: a node change after ready hot-swaps via setTarget (no remount).
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
if (state.phase !== 'ready') return
|
|
309
|
+
if (targetId === lastTargetRef.current) return
|
|
310
|
+
lastTargetRef.current = targetId
|
|
311
|
+
if (targetId === undefined) return
|
|
312
|
+
sendSetTarget(shell, state.windowId, targetId)
|
|
313
|
+
}, [shell, state, targetId])
|
|
314
|
+
|
|
315
|
+
// ── Placement observers: reposition on resize/scroll/anchor-resize.
|
|
316
|
+
useLayoutEffect(() => {
|
|
317
|
+
measure()
|
|
318
|
+
const anchor = anchorRef.current
|
|
319
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null
|
|
320
|
+
if (anchor !== null) ro?.observe(anchor)
|
|
321
|
+
window.addEventListener('resize', measure)
|
|
322
|
+
window.addEventListener('scroll', measure, true)
|
|
323
|
+
return () => {
|
|
324
|
+
ro?.disconnect()
|
|
325
|
+
window.removeEventListener('resize', measure)
|
|
326
|
+
window.removeEventListener('scroll', measure, true)
|
|
327
|
+
}
|
|
328
|
+
}, [measure])
|
|
329
|
+
|
|
330
|
+
// Re-measure on EVERY commit (rect-equality-guarded): a sibling reorder moves
|
|
331
|
+
// the anchor with no resize/scroll event, so the slot must chase it here (§10).
|
|
332
|
+
useLayoutEffect(() => {
|
|
333
|
+
measure()
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div ref={anchorRef} className={props.className} style={props.style}>
|
|
338
|
+
{state.phase !== 'ready' ? props.fallback?.(state) : null}
|
|
339
|
+
</div>
|
|
340
|
+
)
|
|
341
|
+
},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
/** Narrow props back to the bare `ViewSource` discriminated union. */
|
|
345
|
+
function sourceOf(props: ViewHostProps): ViewSource {
|
|
346
|
+
return 'url' in props ? { url: props.url } : { view: props.view }
|
|
347
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The per-shell windows hub (API §10). `ViewStage` mounts once near the root while
|
|
3
|
+
* `ViewHost`s live anywhere, so they rendezvous through a hub keyed by the shared
|
|
4
|
+
* `Shell` instance (a `WeakMap`), not context. It owns the `WindowRegistry`
|
|
5
|
+
* (reactive store behind `useWindows`/`useWindow`, fed by the hosts), the stage
|
|
6
|
+
* controller (the slot layer, published by the stage), and the adopt stash.
|
|
7
|
+
*/
|
|
8
|
+
import type { Shell, Window } from '@astrale-os/shell'
|
|
9
|
+
|
|
10
|
+
import type { AdoptRecord } from './adopt'
|
|
11
|
+
import type { WindowState } from './lifecycle/states'
|
|
12
|
+
import type { StageController } from './placement'
|
|
13
|
+
|
|
14
|
+
import { createAdoptStash } from './adopt'
|
|
15
|
+
|
|
16
|
+
// ── Window registry (reactive store behind useWindows/useWindow) ─────────────
|
|
17
|
+
|
|
18
|
+
/** A host-tracked window: the shell `Window` plus the phase the host observes. */
|
|
19
|
+
interface WindowEntry {
|
|
20
|
+
readonly window: Window
|
|
21
|
+
readonly state: WindowState
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A `useWindow` projection — a window's phase plus its shell `Window` (or none). */
|
|
25
|
+
export type WindowView = WindowState & { readonly window: Window | null }
|
|
26
|
+
|
|
27
|
+
export interface WindowRegistry {
|
|
28
|
+
/** Referentially stable between mutations (useSyncExternalStore contract). */
|
|
29
|
+
getSnapshot(): ReadonlyMap<string, Window>
|
|
30
|
+
/** Stable projection for one window id (null id → a stable "no window"). */
|
|
31
|
+
select(windowId: string | null): WindowView
|
|
32
|
+
subscribe(onChange: () => void): () => void
|
|
33
|
+
/** Host writes: register/transition/drop a window. */
|
|
34
|
+
upsert(windowId: string, window: Window, state: WindowState): void
|
|
35
|
+
patch(windowId: string, state: WindowState): void
|
|
36
|
+
remove(windowId: string): void
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const NO_WINDOW: WindowView = { phase: 'closed', windowId: '', window: null }
|
|
40
|
+
|
|
41
|
+
function createWindowRegistry(): WindowRegistry {
|
|
42
|
+
const entries = new Map<string, WindowEntry>()
|
|
43
|
+
const listeners = new Set<() => void>()
|
|
44
|
+
|
|
45
|
+
// Cached snapshots: rebuilt only on mutation, so repeated getSnapshot/select
|
|
46
|
+
// calls between changes return identical references (no render loop).
|
|
47
|
+
let windowsSnapshot: ReadonlyMap<string, Window> | null = null
|
|
48
|
+
let selectCache = new Map<string, WindowView>()
|
|
49
|
+
|
|
50
|
+
function invalidate() {
|
|
51
|
+
windowsSnapshot = null
|
|
52
|
+
selectCache = new Map()
|
|
53
|
+
for (const l of listeners) l()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
getSnapshot() {
|
|
58
|
+
if (windowsSnapshot === null) {
|
|
59
|
+
const map = new Map<string, Window>()
|
|
60
|
+
for (const [id, entry] of entries) map.set(id, entry.window)
|
|
61
|
+
windowsSnapshot = map
|
|
62
|
+
}
|
|
63
|
+
return windowsSnapshot
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
select(windowId) {
|
|
67
|
+
if (windowId === null) return NO_WINDOW
|
|
68
|
+
const cached = selectCache.get(windowId)
|
|
69
|
+
if (cached !== undefined) return cached
|
|
70
|
+
const entry = entries.get(windowId)
|
|
71
|
+
const view: WindowView = entry
|
|
72
|
+
? { ...entry.state, window: entry.window }
|
|
73
|
+
: { phase: 'closed', windowId, window: null }
|
|
74
|
+
selectCache.set(windowId, view)
|
|
75
|
+
return view
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
subscribe(onChange) {
|
|
79
|
+
listeners.add(onChange)
|
|
80
|
+
return () => {
|
|
81
|
+
listeners.delete(onChange)
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
upsert(windowId, window, state) {
|
|
86
|
+
entries.set(windowId, { window, state })
|
|
87
|
+
invalidate()
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
patch(windowId, state) {
|
|
91
|
+
const entry = entries.get(windowId)
|
|
92
|
+
if (entry === undefined) return
|
|
93
|
+
entries.set(windowId, { window: entry.window, state })
|
|
94
|
+
invalidate()
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
remove(windowId) {
|
|
98
|
+
if (!entries.delete(windowId)) return
|
|
99
|
+
invalidate()
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Per-shell hub ────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export interface StageHub {
|
|
107
|
+
readonly registry: WindowRegistry
|
|
108
|
+
getController(): StageController | null
|
|
109
|
+
/** Publish (stage mount) or retract (stage unmount) the slot layer. */
|
|
110
|
+
setController(controller: StageController | null): void
|
|
111
|
+
/** Await the controller — fires immediately if one is already published. */
|
|
112
|
+
onController(cb: (controller: StageController | null) => void): () => void
|
|
113
|
+
/** Stash an unmounting host's live window; disposes after one macrotask if unadopted. */
|
|
114
|
+
stashForAdopt(key: string, record: AdoptRecord): void
|
|
115
|
+
/** Reclaim a stashed window for a remount of the same source, cancelling its dispose. */
|
|
116
|
+
takeAdopt(key: string): AdoptRecord | null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const HUBS = new WeakMap<Shell, StageHub>()
|
|
120
|
+
|
|
121
|
+
/** The windows hub for a shell — one per shell object, created on first touch. */
|
|
122
|
+
export function getStageHub(shell: Shell): StageHub {
|
|
123
|
+
const existing = HUBS.get(shell)
|
|
124
|
+
if (existing !== undefined) return existing
|
|
125
|
+
|
|
126
|
+
const registry = createWindowRegistry()
|
|
127
|
+
let controller: StageController | null = null
|
|
128
|
+
const waiters = new Set<(c: StageController | null) => void>()
|
|
129
|
+
const adopt = createAdoptStash()
|
|
130
|
+
|
|
131
|
+
const hub: StageHub = {
|
|
132
|
+
registry,
|
|
133
|
+
getController() {
|
|
134
|
+
return controller
|
|
135
|
+
},
|
|
136
|
+
setController(next) {
|
|
137
|
+
controller = next
|
|
138
|
+
for (const w of waiters) w(next)
|
|
139
|
+
},
|
|
140
|
+
onController(cb) {
|
|
141
|
+
waiters.add(cb)
|
|
142
|
+
if (controller !== null) cb(controller)
|
|
143
|
+
return () => {
|
|
144
|
+
waiters.delete(cb)
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
stashForAdopt(key, record) {
|
|
148
|
+
adopt.stash(key, record)
|
|
149
|
+
},
|
|
150
|
+
takeAdopt(key) {
|
|
151
|
+
return adopt.take(key)
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
HUBS.set(shell, hub)
|
|
155
|
+
return hub
|
|
156
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// window — API §10. ViewStage/ViewHost (the physical iframe-slot layer + a host
|
|
2
|
+
// that projects placement into a slot), the reactive window hooks, the child-side
|
|
3
|
+
// target-node + close-guard, and the (G5) view-resolution hook.
|
|
4
|
+
export { ViewStage } from './stage'
|
|
5
|
+
export { ViewHost } from './host'
|
|
6
|
+
export type { ViewHostProps, ViewHostHandle } from './host'
|
|
7
|
+
export { useWindows, useWindowList } from './hooks/windows.hook'
|
|
8
|
+
export type { WindowListEntry } from './hooks/windows.hook'
|
|
9
|
+
export { useWindow } from './hooks/window.hook'
|
|
10
|
+
export type { WindowView } from './hub'
|
|
11
|
+
export { useViewsFor } from './hooks/views-for.hook'
|
|
12
|
+
export type { ViewsForResult } from './hooks/views-for.hook'
|
|
13
|
+
export { useTargetNode } from './hooks/target-node.hook'
|
|
14
|
+
export { useCloseGuard } from './hooks/close-guard.hook'
|
|
15
|
+
export type { CloseGuard, CloseVerdict } from './hooks/close-guard.hook'
|
|
16
|
+
export type { ViewDescriptor, ViewSource, Node } from './lifecycle/mount'
|
|
17
|
+
export type { WindowState } from './lifecycle/states'
|