@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,149 @@
|
|
|
1
|
+
import type { AuthApi, Perms } from '@astrale-os/kernel-client/auth'
|
|
2
|
+
import type { PathLike, Ref } from '@astrale-os/kernel-client/graph'
|
|
3
|
+
|
|
4
|
+
import { rawOf } from '@astrale-os/kernel-client/graph'
|
|
5
|
+
/**
|
|
6
|
+
* useCan (API §4) — deduped standing-grant permission checks. `auth.check`-backed:
|
|
7
|
+
* `undefined` while the first check is in flight (or `on` is idle), then a boolean.
|
|
8
|
+
* Deduped per (principal, perms, nodeId) across the whole tree via the principal's
|
|
9
|
+
* check ledger (folded in here as the dedup substrate). Standing-grant semantics:
|
|
10
|
+
* the dispatched call remains the gatekeeper — this is a UI affordance, not the
|
|
11
|
+
* enforcement point (SPEC §6.2). A local grant/revoke invalidates the ledger.
|
|
12
|
+
*/
|
|
13
|
+
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
|
14
|
+
|
|
15
|
+
import type { NodeValue, SessionView } from './act-as.context'
|
|
16
|
+
|
|
17
|
+
import { isIdleInput } from '../graph/memory/read/idle'
|
|
18
|
+
import { useIdentityBinding } from './act-as.context'
|
|
19
|
+
|
|
20
|
+
const NOOP_UNSUB = (): void => undefined
|
|
21
|
+
|
|
22
|
+
/** A node value carries the id/class/path triple (mirrors the memory's `isNodeValue`). */
|
|
23
|
+
export function isNodeValue(x: unknown): x is NodeValue {
|
|
24
|
+
return typeof x === 'object' && x !== null && 'id' in x && 'class' in x && 'path' in x
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Normalize a `useCan` subject to a dispatchable `Ref` plus a stable dedup key. */
|
|
28
|
+
export function toRef(on: NodeValue | PathLike): { ref: Ref; key: string } {
|
|
29
|
+
if (isNodeValue(on)) return { ref: on.path as Ref, key: `@${on.id}` }
|
|
30
|
+
return { ref: on, key: rawOf(on) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── check ledger (the dedup substrate) ───────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/** One deduped check — its last-known value plus the subscribers watching it. */
|
|
36
|
+
class CheckCell {
|
|
37
|
+
value: boolean | undefined = undefined
|
|
38
|
+
private inFlight = false
|
|
39
|
+
private readonly listeners = new Set<() => void>()
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly auth: AuthApi,
|
|
43
|
+
private readonly perms: Perms,
|
|
44
|
+
private readonly on: Ref,
|
|
45
|
+
) {}
|
|
46
|
+
|
|
47
|
+
subscribe = (onChange: () => void): (() => void) => {
|
|
48
|
+
this.listeners.add(onChange)
|
|
49
|
+
this.ensure()
|
|
50
|
+
return () => this.listeners.delete(onChange)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getSnapshot = (): boolean | undefined => this.value
|
|
54
|
+
|
|
55
|
+
/** Run the check once when the value is unknown and nothing is in flight. */
|
|
56
|
+
ensure(): void {
|
|
57
|
+
if (this.value !== undefined || this.inFlight) return
|
|
58
|
+
this.run()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Force a re-check (local grant/revoke on this principal). */
|
|
62
|
+
invalidate(): void {
|
|
63
|
+
if (this.listeners.size === 0) {
|
|
64
|
+
this.value = undefined // recompute lazily on the next subscribe
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
this.run()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private run(): void {
|
|
71
|
+
this.inFlight = true
|
|
72
|
+
void this.auth
|
|
73
|
+
.check({ on: this.on, perms: this.perms })
|
|
74
|
+
.then(
|
|
75
|
+
(ok) => {
|
|
76
|
+
this.value = ok
|
|
77
|
+
},
|
|
78
|
+
() => {
|
|
79
|
+
// A failed check leaves the value unknown (undefined) — never a false grant.
|
|
80
|
+
this.value = undefined
|
|
81
|
+
},
|
|
82
|
+
)
|
|
83
|
+
.finally(() => {
|
|
84
|
+
this.inFlight = false
|
|
85
|
+
for (const l of this.listeners) l()
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Per-principal cell registry — dedup across the whole tree for one kernel handle. */
|
|
91
|
+
export class CheckLedger {
|
|
92
|
+
private readonly cells = new Map<string, CheckCell>()
|
|
93
|
+
|
|
94
|
+
constructor(private readonly auth: AuthApi) {}
|
|
95
|
+
|
|
96
|
+
cell(key: string, perms: Perms, on: Ref): CheckCell {
|
|
97
|
+
let cell = this.cells.get(key)
|
|
98
|
+
if (cell === undefined) {
|
|
99
|
+
cell = new CheckCell(this.auth, perms, on)
|
|
100
|
+
this.cells.set(key, cell)
|
|
101
|
+
}
|
|
102
|
+
return cell
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Re-check every observed cell — conservative Tier-0 sweep after a local grant/revoke. */
|
|
106
|
+
invalidateAll(): void {
|
|
107
|
+
for (const cell of this.cells.values()) cell.invalidate()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// One ledger per bound kernel object → per principal. `session.as(cred)` mints a
|
|
112
|
+
// fresh view per ActAs, so acted-as principals never share a ledger with the root.
|
|
113
|
+
const ledgers = new WeakMap<SessionView, CheckLedger>()
|
|
114
|
+
|
|
115
|
+
export function ledgerFor(kernel: SessionView): CheckLedger {
|
|
116
|
+
let ledger = ledgers.get(kernel)
|
|
117
|
+
if (ledger === undefined) {
|
|
118
|
+
ledger = new CheckLedger(kernel.auth)
|
|
119
|
+
ledgers.set(kernel, ledger)
|
|
120
|
+
}
|
|
121
|
+
return ledger
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── useCan ────────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export function useCan(
|
|
127
|
+
perms: Perms,
|
|
128
|
+
on: NodeValue | PathLike | null | undefined,
|
|
129
|
+
): boolean | undefined {
|
|
130
|
+
const binding = useIdentityBinding()
|
|
131
|
+
const ref = isIdleInput(on) ? null : toRef(on)
|
|
132
|
+
|
|
133
|
+
// The shared cell for (principal, perms, nodeId): identical to every other
|
|
134
|
+
// useCan reading the same triple, so one check serves the whole tree.
|
|
135
|
+
const cell = useMemo(
|
|
136
|
+
() =>
|
|
137
|
+
ref === null ? null : ledgerFor(binding.kernel).cell(`${perms}\0${ref.key}`, perms, ref.ref),
|
|
138
|
+
// ref is rebuilt per render; its `key` string is the stable identity.
|
|
139
|
+
[binding.kernel, perms, ref?.key],
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const subscribe = useCallback(
|
|
143
|
+
(onChange: () => void) => (cell === null ? NOOP_UNSUB : cell.subscribe(onChange)),
|
|
144
|
+
[cell],
|
|
145
|
+
)
|
|
146
|
+
const getSnapshot = useCallback((): boolean | undefined => cell?.getSnapshot(), [cell])
|
|
147
|
+
|
|
148
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
149
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// auth — API §4 (useSelf, useCan, useAuth, ActAs). See SHELL_REACT_API.md.
|
|
2
|
+
export { ActAs } from './act-as.context'
|
|
3
|
+
export type { ActAsProps } from './act-as.context'
|
|
4
|
+
export { useAuth } from './auth.hook'
|
|
5
|
+
export { useCan } from './can.hook'
|
|
6
|
+
export { useSelf } from './self.hook'
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NodeValue } from './act-as.context'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useSelf (API §4) — the caller's identity node, resolved at boot and read
|
|
5
|
+
* synchronously (never suspends). An <ActAs> subtree returns the acted-as
|
|
6
|
+
* identity. FLAG: today this is the boot-resolved whoami snapshot; it becomes a
|
|
7
|
+
* live memory shape once the graph plane subscribes the self node — no signature
|
|
8
|
+
* change.
|
|
9
|
+
*/
|
|
10
|
+
import { useIdentityBinding } from './act-as.context'
|
|
11
|
+
|
|
12
|
+
export function useSelf(): NodeValue {
|
|
13
|
+
return useIdentityBinding().self
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSessionKernel (API §4) — the dispatch-floor resolver. Inside an `<ActAs>` it
|
|
3
|
+
* is the acted-as principal's bound session view; otherwise the root
|
|
4
|
+
* `shell.kernel`. Every dispatch floor (schema bindings, useCall/useStream, useMove,
|
|
5
|
+
* typed method dispatch) routes through this so a call runs as the subtree's
|
|
6
|
+
* principal — not the root — and its scoped memory.
|
|
7
|
+
*/
|
|
8
|
+
import { useContext } from 'react'
|
|
9
|
+
|
|
10
|
+
import type { SessionView } from './act-as.context'
|
|
11
|
+
|
|
12
|
+
import { ShellContext } from '../session/session.context'
|
|
13
|
+
import { IdentityContext } from './act-as.context'
|
|
14
|
+
|
|
15
|
+
/** The EFFECTIVE dispatch kernel for this subtree. Throws outside a provider. */
|
|
16
|
+
export function useSessionKernel(): SessionView {
|
|
17
|
+
const binding = useContext(IdentityContext)
|
|
18
|
+
const runtime = useContext(ShellContext)
|
|
19
|
+
if (binding !== null) return binding.kernel
|
|
20
|
+
if (runtime === null) {
|
|
21
|
+
throw new Error('session hooks must be used within a <ShellProvider>')
|
|
22
|
+
}
|
|
23
|
+
return runtime.shell.kernel
|
|
24
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Action envelope (API §1.2) — a graph-plane primitive, not a context (rule
|
|
3
|
+
* 5). Holds the `Action<A,R>` type (public — it appears in every write/dispatch
|
|
4
|
+
* hook signature) and the `useAction` stateful-callable factory (internal): a
|
|
5
|
+
* render-stable callable whose pending/error/result snapshot is re-stamped each
|
|
6
|
+
* render, safe in deps arrays.
|
|
7
|
+
*/
|
|
8
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react'
|
|
9
|
+
|
|
10
|
+
/** The stateful callable every write/dispatch hook returns. */
|
|
11
|
+
export interface Action<A extends unknown[], R> {
|
|
12
|
+
/** Invoke; rejects on failure AND records it on `.error`. */
|
|
13
|
+
(...args: A): Promise<R>
|
|
14
|
+
/** ≥1 invocation in flight (the OR across concurrent calls). */
|
|
15
|
+
readonly pending: boolean
|
|
16
|
+
/** Last rejection; cleared when the next invocation starts. */
|
|
17
|
+
readonly error: unknown
|
|
18
|
+
/** Last successful resolution. */
|
|
19
|
+
readonly result: R | undefined
|
|
20
|
+
/** Clears error/result. */
|
|
21
|
+
reset(): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Same shape, minus `readonly`, so the render step can stamp the snapshot on. */
|
|
25
|
+
interface MutableAction<A extends unknown[], R> {
|
|
26
|
+
(...args: A): Promise<R>
|
|
27
|
+
pending: boolean
|
|
28
|
+
error: unknown
|
|
29
|
+
result: R | undefined
|
|
30
|
+
reset(): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ActionSnapshot<R> {
|
|
34
|
+
readonly pending: boolean
|
|
35
|
+
readonly error: unknown
|
|
36
|
+
readonly result: R | undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IDLE_SNAPSHOT: ActionSnapshot<unknown> = {
|
|
40
|
+
pending: false,
|
|
41
|
+
error: undefined,
|
|
42
|
+
result: undefined,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type ActionEvent<R> =
|
|
46
|
+
| { readonly type: 'start' }
|
|
47
|
+
| { readonly type: 'resolve'; readonly value: R; readonly inFlight: number }
|
|
48
|
+
| { readonly type: 'reject'; readonly error: unknown; readonly inFlight: number }
|
|
49
|
+
| { readonly type: 'reset' }
|
|
50
|
+
|
|
51
|
+
function actionReducer<R>(state: ActionSnapshot<R>, event: ActionEvent<R>): ActionSnapshot<R> {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case 'start':
|
|
54
|
+
// §1.2: a new invocation clears the error and keeps the last result.
|
|
55
|
+
return { pending: true, error: undefined, result: state.result }
|
|
56
|
+
case 'resolve':
|
|
57
|
+
// Latest settlement wins: a success records the result and clears error.
|
|
58
|
+
return { pending: event.inFlight > 0, error: undefined, result: event.value }
|
|
59
|
+
case 'reject':
|
|
60
|
+
return { pending: event.inFlight > 0, error: event.error, result: state.result }
|
|
61
|
+
case 'reset':
|
|
62
|
+
return { pending: state.pending, error: undefined, result: undefined }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Wrap an async function in the Action envelope. Concurrent invocations are
|
|
68
|
+
* allowed — each returns its own promise; `pending` is the OR of the in-flight
|
|
69
|
+
* count and `result`/`error` reflect the latest settlement. The callable keeps
|
|
70
|
+
* ONE identity for the hook's lifetime (safe in deps arrays); its snapshot
|
|
71
|
+
* fields are re-stamped each render, so reads stay current across settlements.
|
|
72
|
+
*/
|
|
73
|
+
export function useAction<A extends unknown[], R>(fn: (...args: A) => Promise<R>): Action<A, R> {
|
|
74
|
+
// Latest-closure ref so the stable callable always dispatches the current fn.
|
|
75
|
+
const fnRef = useRef(fn)
|
|
76
|
+
fnRef.current = fn
|
|
77
|
+
|
|
78
|
+
const [snapshot, dispatch] = useReducer(actionReducer<R>, IDLE_SNAPSHOT as ActionSnapshot<R>)
|
|
79
|
+
const inFlight = useRef(0)
|
|
80
|
+
const alive = useRef(true)
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
alive.current = true
|
|
83
|
+
return () => {
|
|
84
|
+
alive.current = false
|
|
85
|
+
}
|
|
86
|
+
}, [])
|
|
87
|
+
|
|
88
|
+
const call = useCallback((...args: A): Promise<R> => {
|
|
89
|
+
inFlight.current += 1
|
|
90
|
+
dispatch({ type: 'start' })
|
|
91
|
+
let promise: Promise<R>
|
|
92
|
+
try {
|
|
93
|
+
promise = Promise.resolve(fnRef.current(...args))
|
|
94
|
+
} catch (err) {
|
|
95
|
+
promise = Promise.reject(err)
|
|
96
|
+
}
|
|
97
|
+
void promise.then(
|
|
98
|
+
(value) => {
|
|
99
|
+
inFlight.current -= 1
|
|
100
|
+
if (alive.current) dispatch({ type: 'resolve', value, inFlight: inFlight.current })
|
|
101
|
+
},
|
|
102
|
+
(err: unknown) => {
|
|
103
|
+
inFlight.current -= 1
|
|
104
|
+
if (alive.current) dispatch({ type: 'reject', error: err, inFlight: inFlight.current })
|
|
105
|
+
},
|
|
106
|
+
)
|
|
107
|
+
return promise
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
const reset = useCallback(() => dispatch({ type: 'reset' }), [])
|
|
111
|
+
|
|
112
|
+
// One render-stable callable: built once (call/reset are themselves stable),
|
|
113
|
+
// then its snapshot fields re-stamped every render. Re-renders are driven by
|
|
114
|
+
// the reducer, so the stamped pending/error/result stay fresh WITHOUT minting
|
|
115
|
+
// a new function identity — the documented "safe in deps arrays" guarantee.
|
|
116
|
+
const actionRef = useRef<MutableAction<A, R> | null>(null)
|
|
117
|
+
if (actionRef.current === null) {
|
|
118
|
+
const action = ((...args: A) => call(...args)) as MutableAction<A, R>
|
|
119
|
+
action.reset = reset
|
|
120
|
+
actionRef.current = action
|
|
121
|
+
}
|
|
122
|
+
const action = actionRef.current
|
|
123
|
+
action.pending = snapshot.pending
|
|
124
|
+
action.error = snapshot.error
|
|
125
|
+
action.result = snapshot.result
|
|
126
|
+
return action as Action<A, R>
|
|
127
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAsyncResource (API §1.1) — a one-shot async read projected into the §1 read
|
|
3
|
+
* envelope (`ReadState`): the non-memory sibling of `useRead`, stale-guarded and
|
|
4
|
+
* `refetch`-able, with a `null` loader as idle. Shared by `application/` and the
|
|
5
|
+
* window `useViewsFor` dispatch; homed here (below both) to keep rule 9 acyclic.
|
|
6
|
+
*/
|
|
7
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
8
|
+
|
|
9
|
+
import type { ReadState } from './memory/read/envelope'
|
|
10
|
+
|
|
11
|
+
export interface AsyncResource<T> extends ReadState {
|
|
12
|
+
readonly data: T
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface AsyncState<T> {
|
|
16
|
+
readonly data: T
|
|
17
|
+
readonly pending: boolean
|
|
18
|
+
readonly error: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Subscribe a component to `load()`. `null` loader → idle. The loader runs on
|
|
23
|
+
* mount and whenever `deps` change (stale responses discarded); last-good data is
|
|
24
|
+
* retained through a pending refetch or a failure (§1.1).
|
|
25
|
+
*/
|
|
26
|
+
export function useAsyncResource<T>(
|
|
27
|
+
load: (() => Promise<T>) | null,
|
|
28
|
+
empty: T,
|
|
29
|
+
deps: readonly unknown[],
|
|
30
|
+
): AsyncResource<T> {
|
|
31
|
+
const [state, setState] = useState<AsyncState<T>>({
|
|
32
|
+
data: empty,
|
|
33
|
+
pending: load !== null,
|
|
34
|
+
error: undefined,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const loadRef = useRef(load)
|
|
38
|
+
loadRef.current = load
|
|
39
|
+
const emptyRef = useRef(empty)
|
|
40
|
+
emptyRef.current = empty
|
|
41
|
+
const seq = useRef(0)
|
|
42
|
+
const alive = useRef(true)
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
alive.current = true
|
|
45
|
+
return () => {
|
|
46
|
+
alive.current = false
|
|
47
|
+
}
|
|
48
|
+
}, [])
|
|
49
|
+
|
|
50
|
+
const refetch = useCallback((): Promise<void> => {
|
|
51
|
+
const run = loadRef.current
|
|
52
|
+
if (run === null) {
|
|
53
|
+
setState({ data: emptyRef.current, pending: false, error: undefined })
|
|
54
|
+
return Promise.resolve()
|
|
55
|
+
}
|
|
56
|
+
const my = ++seq.current
|
|
57
|
+
setState((s) => ({ data: s.data, pending: true, error: undefined }))
|
|
58
|
+
return run().then(
|
|
59
|
+
(data) => {
|
|
60
|
+
if (alive.current && my === seq.current)
|
|
61
|
+
setState({ data, pending: false, error: undefined })
|
|
62
|
+
},
|
|
63
|
+
(error: unknown) => {
|
|
64
|
+
if (alive.current && my === seq.current) {
|
|
65
|
+
setState((s) => ({ data: s.data, pending: false, error }))
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
)
|
|
69
|
+
}, [])
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
void refetch()
|
|
73
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
74
|
+
}, deps)
|
|
75
|
+
|
|
76
|
+
return useMemo(
|
|
77
|
+
() => ({
|
|
78
|
+
data: state.data,
|
|
79
|
+
pending: state.pending,
|
|
80
|
+
error: state.error,
|
|
81
|
+
live: false,
|
|
82
|
+
seq: 0,
|
|
83
|
+
refetch,
|
|
84
|
+
}),
|
|
85
|
+
[state.data, state.pending, state.error, refetch],
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useCall (API §7) — unary domain-function dispatch over `kernel.call`, in the
|
|
3
|
+
* Action envelope. No result caching, no liveness — domain function reads are not
|
|
4
|
+
* structurally invalidatable (FRONTEND §5.7); a consumer that needs freshness
|
|
5
|
+
* owns its own refetch policy. Dispatch is a graph operation (rule 2).
|
|
6
|
+
*/
|
|
7
|
+
import type { CallFns, FnMap } from '@astrale-os/kernel-client'
|
|
8
|
+
|
|
9
|
+
import type { Action } from '../action'
|
|
10
|
+
|
|
11
|
+
import { useSessionKernel } from '../../auth/session-kernel'
|
|
12
|
+
import { useAction } from '../action'
|
|
13
|
+
|
|
14
|
+
export interface CallOptions {
|
|
15
|
+
/** Per-call timeout (ms), forwarded to the kernel dispatch. */
|
|
16
|
+
timeout?: number
|
|
17
|
+
/** Side-channel notified on rejection (the Action also records `.error`). */
|
|
18
|
+
onError?(error: unknown): void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Unary domain-function dispatch as an {@link Action}. `method` is a typed FnMap
|
|
23
|
+
* `call` path or any string (the untyped floor); `null` yields an idle action
|
|
24
|
+
* that rejects if invoked. Params/result are typed from the FnMap entry.
|
|
25
|
+
*/
|
|
26
|
+
export function useCall<K extends CallFns<FnMap>>(
|
|
27
|
+
method: K | null,
|
|
28
|
+
opts?: CallOptions,
|
|
29
|
+
): Action<[FnMap[K]['params']], FnMap[K]['result']> {
|
|
30
|
+
const kernel = useSessionKernel()
|
|
31
|
+
// A fresh closure per render captures the current method/opts; `useAction`
|
|
32
|
+
// keeps ONE stable callable and always dispatches this latest closure.
|
|
33
|
+
return useAction(async (params: FnMap[K]['params']): Promise<FnMap[K]['result']> => {
|
|
34
|
+
if (method === null) throw new Error('useCall: method is null (idle action invoked)')
|
|
35
|
+
try {
|
|
36
|
+
return await kernel.call(method, params, { timeout: opts?.timeout })
|
|
37
|
+
} catch (error) {
|
|
38
|
+
opts?.onError?.(error)
|
|
39
|
+
throw error
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type { FnMap, StreamFns } from '@astrale-os/kernel-client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useStream (API §7) — streaming domain-function dispatch over `kernel.stream`,
|
|
5
|
+
* an AsyncIterable driven by an explicit start/cancel handle. A superseding
|
|
6
|
+
* `start` (or unmount) closes the prior run and discards its late chunks/errors;
|
|
7
|
+
* never live. Dispatch is a graph operation (rule 2).
|
|
8
|
+
*/
|
|
9
|
+
import { useCallback, useEffect, useReducer, useRef } from 'react'
|
|
10
|
+
|
|
11
|
+
import { useSessionKernel } from '../../auth/session-kernel'
|
|
12
|
+
|
|
13
|
+
export interface StreamOptions<T> {
|
|
14
|
+
/** Notified per chunk as it arrives (in addition to `chunks` accumulation). */
|
|
15
|
+
onChunk?(chunk: T): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StreamHandle<T> {
|
|
19
|
+
/** Begin a run: resets chunks, then resolves when the stream ends / rejects on failure. */
|
|
20
|
+
start(params: unknown): Promise<void>
|
|
21
|
+
/** Chunks accumulated in the current run; reset on the next `start`. */
|
|
22
|
+
readonly chunks: readonly T[]
|
|
23
|
+
/** The most recent chunk, or null before the first. */
|
|
24
|
+
readonly last: T | null
|
|
25
|
+
/** A run is in flight. */
|
|
26
|
+
readonly streaming: boolean
|
|
27
|
+
/** Last run failure; retained until the next `start`. */
|
|
28
|
+
readonly error: unknown
|
|
29
|
+
/** Close the stream (returns the iterator); a later `start` begins fresh. */
|
|
30
|
+
cancel(): void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface StreamSnapshot<T> {
|
|
34
|
+
readonly chunks: readonly T[]
|
|
35
|
+
readonly last: T | null
|
|
36
|
+
readonly streaming: boolean
|
|
37
|
+
readonly error: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const IDLE_STREAM: StreamSnapshot<unknown> = {
|
|
41
|
+
chunks: [],
|
|
42
|
+
last: null,
|
|
43
|
+
streaming: false,
|
|
44
|
+
error: undefined,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type StreamEvent<T> =
|
|
48
|
+
| { readonly type: 'start' }
|
|
49
|
+
| { readonly type: 'chunk'; readonly value: T }
|
|
50
|
+
| { readonly type: 'end' }
|
|
51
|
+
| { readonly type: 'error'; readonly error: unknown }
|
|
52
|
+
| { readonly type: 'cancel' }
|
|
53
|
+
|
|
54
|
+
function streamReducer<T>(state: StreamSnapshot<T>, event: StreamEvent<T>): StreamSnapshot<T> {
|
|
55
|
+
switch (event.type) {
|
|
56
|
+
case 'start':
|
|
57
|
+
return { chunks: [], last: null, streaming: true, error: undefined }
|
|
58
|
+
case 'chunk':
|
|
59
|
+
return {
|
|
60
|
+
chunks: [...state.chunks, event.value],
|
|
61
|
+
last: event.value,
|
|
62
|
+
streaming: true,
|
|
63
|
+
error: undefined,
|
|
64
|
+
}
|
|
65
|
+
case 'end':
|
|
66
|
+
return { ...state, streaming: false }
|
|
67
|
+
case 'error':
|
|
68
|
+
// Chunks accumulated before the failure are preserved.
|
|
69
|
+
return { ...state, streaming: false, error: event.error }
|
|
70
|
+
case 'cancel':
|
|
71
|
+
return { ...state, streaming: false }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Same shape, minus `readonly`, so the render step can re-stamp the snapshot. */
|
|
76
|
+
interface MutableStreamHandle<T> {
|
|
77
|
+
start(params: unknown): Promise<void>
|
|
78
|
+
chunks: readonly T[]
|
|
79
|
+
last: T | null
|
|
80
|
+
streaming: boolean
|
|
81
|
+
error: unknown
|
|
82
|
+
cancel(): void
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface StreamRun<T> {
|
|
86
|
+
readonly controller: AbortController
|
|
87
|
+
iterator: AsyncIterator<T> | null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Streaming domain-function dispatch. `start(params)` opens a run over
|
|
92
|
+
* `kernel.stream`; chunks accumulate on `chunks`; `cancel()` closes the iterator
|
|
93
|
+
* and a later `start` begins fresh. A superseding `start` (or unmount) closes the
|
|
94
|
+
* prior run and discards its late chunks/errors — only the current run drives
|
|
95
|
+
* state. Never live; the handle keeps one identity for the hook's lifetime.
|
|
96
|
+
*/
|
|
97
|
+
export function useStream<K extends StreamFns<FnMap>>(
|
|
98
|
+
method: K | null,
|
|
99
|
+
opts?: StreamOptions<FnMap[K]['result']>,
|
|
100
|
+
): StreamHandle<FnMap[K]['result']> {
|
|
101
|
+
type T = FnMap[K]['result']
|
|
102
|
+
const kernel = useSessionKernel()
|
|
103
|
+
|
|
104
|
+
// Latest-closure refs so the stable start/cancel dispatch the current inputs.
|
|
105
|
+
const kernelRef = useRef(kernel)
|
|
106
|
+
kernelRef.current = kernel
|
|
107
|
+
const methodRef = useRef(method)
|
|
108
|
+
methodRef.current = method
|
|
109
|
+
const optsRef = useRef(opts)
|
|
110
|
+
optsRef.current = opts
|
|
111
|
+
|
|
112
|
+
const [snapshot, dispatch] = useReducer(streamReducer<T>, IDLE_STREAM as StreamSnapshot<T>)
|
|
113
|
+
|
|
114
|
+
// `runId` supersession token; only the run whose id is still current drives state.
|
|
115
|
+
const runIdRef = useRef(0)
|
|
116
|
+
const currentRef = useRef<StreamRun<T> | null>(null)
|
|
117
|
+
const alive = useRef(true)
|
|
118
|
+
|
|
119
|
+
const stop = useCallback(() => {
|
|
120
|
+
const run = currentRef.current
|
|
121
|
+
if (run !== null) {
|
|
122
|
+
run.controller.abort()
|
|
123
|
+
void run.iterator?.return?.(undefined)
|
|
124
|
+
currentRef.current = null
|
|
125
|
+
}
|
|
126
|
+
}, [])
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
alive.current = true
|
|
130
|
+
return () => {
|
|
131
|
+
alive.current = false
|
|
132
|
+
stop()
|
|
133
|
+
}
|
|
134
|
+
}, [stop])
|
|
135
|
+
|
|
136
|
+
const cancel = useCallback(() => {
|
|
137
|
+
runIdRef.current += 1
|
|
138
|
+
stop()
|
|
139
|
+
if (alive.current) dispatch({ type: 'cancel' })
|
|
140
|
+
}, [stop])
|
|
141
|
+
|
|
142
|
+
const start = useCallback(
|
|
143
|
+
(params: unknown): Promise<void> => {
|
|
144
|
+
const method_ = methodRef.current
|
|
145
|
+
if (method_ === null) {
|
|
146
|
+
return Promise.reject(new Error('useStream: method is null (idle handle started)'))
|
|
147
|
+
}
|
|
148
|
+
runIdRef.current += 1
|
|
149
|
+
const runId = runIdRef.current
|
|
150
|
+
stop() // close any prior run before opening the new one
|
|
151
|
+
dispatch({ type: 'start' })
|
|
152
|
+
|
|
153
|
+
const controller = new AbortController()
|
|
154
|
+
const run: StreamRun<T> = { controller, iterator: null }
|
|
155
|
+
currentRef.current = run
|
|
156
|
+
const current = (): boolean => runIdRef.current === runId
|
|
157
|
+
|
|
158
|
+
return (async () => {
|
|
159
|
+
try {
|
|
160
|
+
const iterable = await kernelRef.current.stream(method_, params, {
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
})
|
|
163
|
+
if (!current()) return
|
|
164
|
+
const iterator = iterable[Symbol.asyncIterator]()
|
|
165
|
+
run.iterator = iterator
|
|
166
|
+
for (;;) {
|
|
167
|
+
const next = await iterator.next()
|
|
168
|
+
if (!current()) return
|
|
169
|
+
if (next.done === true) break
|
|
170
|
+
if (alive.current) dispatch({ type: 'chunk', value: next.value })
|
|
171
|
+
optsRef.current?.onChunk?.(next.value)
|
|
172
|
+
}
|
|
173
|
+
if (current()) {
|
|
174
|
+
currentRef.current = null
|
|
175
|
+
if (alive.current) dispatch({ type: 'end' })
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (!current()) return // a superseded/cancelled run's failure is discarded
|
|
179
|
+
currentRef.current = null
|
|
180
|
+
if (alive.current) dispatch({ type: 'error', error })
|
|
181
|
+
throw error
|
|
182
|
+
}
|
|
183
|
+
})()
|
|
184
|
+
},
|
|
185
|
+
[stop],
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
// One render-stable handle: built once (start/cancel are stable), then its
|
|
189
|
+
// snapshot fields re-stamped each render so reads stay current across chunks.
|
|
190
|
+
const handleRef = useRef<MutableStreamHandle<T> | null>(null)
|
|
191
|
+
if (handleRef.current === null) {
|
|
192
|
+
handleRef.current = { start, cancel } as MutableStreamHandle<T>
|
|
193
|
+
}
|
|
194
|
+
const handle = handleRef.current
|
|
195
|
+
handle.chunks = snapshot.chunks
|
|
196
|
+
handle.last = snapshot.last
|
|
197
|
+
handle.streaming = snapshot.streaming
|
|
198
|
+
handle.error = snapshot.error
|
|
199
|
+
return handle as StreamHandle<T>
|
|
200
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GraphMemory } from '@astrale-os/kernel-client/store'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GraphContext (ex MemoryContext) — the root of the graph plane. Seats the base
|
|
5
|
+
* (default-scope) memory the provider builds; the SAME handle the graph/schema/
|
|
6
|
+
* nav hooks read through `memory/scope.ts`, and selection's liveness source.
|
|
7
|
+
* Under `<ActAs>` the read hooks re-scope via `forScope`; selection reads the
|
|
8
|
+
* base scope here.
|
|
9
|
+
*/
|
|
10
|
+
import { createContext } from 'react'
|
|
11
|
+
|
|
12
|
+
export type { GraphMemory }
|
|
13
|
+
export const GraphContext = createContext<GraphMemory | null>(null)
|
|
14
|
+
GraphContext.displayName = 'AstraleGraphContext'
|