@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,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level surface for the schema module (API §6): the read envelopes
|
|
3
|
+
* (`TypedNodeResult`/`TypedChildrenResult`/`EndpointResult`) and the method /
|
|
4
|
+
* function contract projections. Param inference mirrors the client's proxy
|
|
5
|
+
* types (Zod optionality preserved), re-derived here because it is not exported.
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
import type {
|
|
9
|
+
AnyBoundNode,
|
|
10
|
+
BoundNode,
|
|
11
|
+
DefOf,
|
|
12
|
+
EdgeNames,
|
|
13
|
+
EndpointsOf,
|
|
14
|
+
NodeRefInput,
|
|
15
|
+
TypeNames,
|
|
16
|
+
} from '@astrale-os/kernel-client'
|
|
17
|
+
import type { z } from '@astrale-os/kernel-core'
|
|
18
|
+
import type {
|
|
19
|
+
AllMethods,
|
|
20
|
+
ExtractMethodNames,
|
|
21
|
+
ExtractMethodParams,
|
|
22
|
+
ExtractMethodReturnValue,
|
|
23
|
+
Schema,
|
|
24
|
+
} from '@astrale-os/kernel-dsl'
|
|
25
|
+
|
|
26
|
+
import type { Action } from '../graph/action'
|
|
27
|
+
import type { ReadState, Windowed } from '../graph/memory/read/envelope'
|
|
28
|
+
|
|
29
|
+
// ── Read envelopes ──────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface TypedNodeResult<S extends Schema, T extends TypeNames<S>> extends ReadState {
|
|
32
|
+
readonly node: BoundNode<DefOf<S, T>, S> | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface TypedChildrenResult<S extends Schema, T extends TypeNames<S>>
|
|
36
|
+
extends ReadState, Windowed {
|
|
37
|
+
readonly children: readonly BoundNode<DefOf<S, T>, S>[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Cardinality decides the envelope key: single|null for `1`/`0..1`, array otherwise. */
|
|
41
|
+
export type EndpointResult<S extends Schema, E extends EdgeNames<S>, Side extends 'from' | 'to'> =
|
|
42
|
+
EndpointsOf<S, E, Side> extends readonly unknown[]
|
|
43
|
+
? ReadState & { readonly nodes: EndpointsOf<S, E, Side> }
|
|
44
|
+
: ReadState & { readonly node: EndpointsOf<S, E, Side> }
|
|
45
|
+
|
|
46
|
+
/** Untyped traversal fallback — mirrors the raw `neighbors` shorthand. */
|
|
47
|
+
export interface UntypedEndpointResult<S extends Schema> extends ReadState, Windowed {
|
|
48
|
+
readonly nodes: readonly AnyBoundNode<S>[]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Method contracts ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/** Dispatchable method names on `type` (own + inherited). */
|
|
54
|
+
export type MethodNames<S extends Schema, T extends TypeNames<S>> = ExtractMethodNames<DefOf<S, T>>
|
|
55
|
+
|
|
56
|
+
type OptionalKeys<P> = {
|
|
57
|
+
[K in keyof P]: P[K] extends z.ZodOptional<any> | z.ZodDefault<any> ? K : never
|
|
58
|
+
}[keyof P]
|
|
59
|
+
type RequiredKeys<P> = Exclude<keyof P, OptionalKeys<P>>
|
|
60
|
+
type InferParams<P> = {
|
|
61
|
+
[K in RequiredKeys<P>]: P[K] extends z.ZodType<infer T> ? T : never
|
|
62
|
+
} & {
|
|
63
|
+
[K in OptionalKeys<P>]?: P[K] extends z.ZodType<infer T> ? T : never
|
|
64
|
+
}
|
|
65
|
+
type Simplify<T> = { [K in keyof T]: T[K] }
|
|
66
|
+
|
|
67
|
+
/** Runtime params object for method `M` on `type`. */
|
|
68
|
+
export type MethodParams<
|
|
69
|
+
S extends Schema,
|
|
70
|
+
T extends TypeNames<S>,
|
|
71
|
+
M extends MethodNames<S, T>,
|
|
72
|
+
> = Simplify<InferParams<ExtractMethodParams<DefOf<S, T>, M & string>>>
|
|
73
|
+
|
|
74
|
+
/** Resolved return value of method `M` on `type`. */
|
|
75
|
+
export type MethodResult<
|
|
76
|
+
S extends Schema,
|
|
77
|
+
T extends TypeNames<S>,
|
|
78
|
+
M extends MethodNames<S, T>,
|
|
79
|
+
> = ExtractMethodReturnValue<DefOf<S, T>, M & string>
|
|
80
|
+
|
|
81
|
+
/** True when method `M` on `type` is static. */
|
|
82
|
+
export type MethodIsStatic<
|
|
83
|
+
S extends Schema,
|
|
84
|
+
T extends TypeNames<S>,
|
|
85
|
+
M extends MethodNames<S, T>,
|
|
86
|
+
> = M extends keyof AllMethods<DefOf<S, T>>
|
|
87
|
+
? AllMethods<DefOf<S, T>>[M] extends { config: { static: true } }
|
|
88
|
+
? true
|
|
89
|
+
: false
|
|
90
|
+
: false
|
|
91
|
+
|
|
92
|
+
/** Action for a `on`-bound or static method: `action(params)`. */
|
|
93
|
+
export type BoundMethodAction<
|
|
94
|
+
S extends Schema,
|
|
95
|
+
T extends TypeNames<S>,
|
|
96
|
+
M extends MethodNames<S, T>,
|
|
97
|
+
> = Action<[MethodParams<S, T, M>], MethodResult<S, T, M>>
|
|
98
|
+
|
|
99
|
+
/** Action for an unbound instance method: `action(ref, params)`. */
|
|
100
|
+
export type TargetedMethodAction<
|
|
101
|
+
S extends Schema,
|
|
102
|
+
T extends TypeNames<S>,
|
|
103
|
+
M extends MethodNames<S, T>,
|
|
104
|
+
> = Action<[ref: NodeRefInput, params: MethodParams<S, T, M>], MethodResult<S, T, M>>
|
|
105
|
+
|
|
106
|
+
/** Static → `action(params)`; instance → `action(ref, params)` (no `on`). */
|
|
107
|
+
export type AutoMethodAction<
|
|
108
|
+
S extends Schema,
|
|
109
|
+
T extends TypeNames<S>,
|
|
110
|
+
M extends MethodNames<S, T>,
|
|
111
|
+
> =
|
|
112
|
+
MethodIsStatic<S, T, M> extends true ? BoundMethodAction<S, T, M> : TargetedMethodAction<S, T, M>
|
|
113
|
+
|
|
114
|
+
// ── Function contracts ──────────────────────────────────────────────────────────────
|
|
115
|
+
//
|
|
116
|
+
// DEGRADATION (§6 deviation — tracked to kernel-dsl): `defineSchema`
|
|
117
|
+
// (kernel/dsl/src/schema/define.ts) does NOT make the `functions` record generic
|
|
118
|
+
// — `Schema.functions` is the widened `Record<string, FuncDef>`, and `FuncDef`'s
|
|
119
|
+
// input/output default to `z.ZodType`. So for a real domain schema `S['functions']
|
|
120
|
+
// [F]` is a bare `FuncDef`: `FunctionNames<S>` widens to `string` (ANY name
|
|
121
|
+
// compiles), and `FunctionParams`/`FunctionResult` resolve to `unknown`. These
|
|
122
|
+
// projections are correct in shape; the phantom type-safety is upstream. The fix
|
|
123
|
+
// belongs in kernel-dsl (make `defineSchema` generic over the functions record →
|
|
124
|
+
// `Schema<I, C, F>`) and is deferred here: it is a cross-ecosystem change (a third
|
|
125
|
+
// `Schema` type param threads through every domain's declared type) carrying the
|
|
126
|
+
// declaration-emit size hazard `imports` was widened to avoid (schema.ts). Until
|
|
127
|
+
// then, treat `useFunction` names/params/results as UNCHECKED at real call sites.
|
|
128
|
+
|
|
129
|
+
/** Standalone top-level function names of the schema (widens to `string` today — see above). */
|
|
130
|
+
export type FunctionNames<S extends Schema> = keyof S['functions'] & string
|
|
131
|
+
|
|
132
|
+
/** Input value of standalone function `F` (`unknown` for a widened `FuncDef` — see above). */
|
|
133
|
+
export type FunctionParams<
|
|
134
|
+
S extends Schema,
|
|
135
|
+
F extends FunctionNames<S>,
|
|
136
|
+
> = S['functions'][F] extends { config: { input: infer I } }
|
|
137
|
+
? I extends z.ZodType
|
|
138
|
+
? z.infer<I>
|
|
139
|
+
: never
|
|
140
|
+
: never
|
|
141
|
+
|
|
142
|
+
/** Output value of standalone function `F` (`unknown` for a widened `FuncDef` — see above). */
|
|
143
|
+
export type FunctionResult<
|
|
144
|
+
S extends Schema,
|
|
145
|
+
F extends FunctionNames<S>,
|
|
146
|
+
> = S['functions'][F] extends { config: { output: infer O } }
|
|
147
|
+
? O extends z.ZodType
|
|
148
|
+
? z.infer<O>
|
|
149
|
+
: never
|
|
150
|
+
: never
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-routed writes (API §6). Wraps a session view so `updateNode`/`createNode`
|
|
3
|
+
* route through the shared `GraphMemory`'s optimistic write shorthands — so
|
|
4
|
+
* `BoundNode.update` / `SchemaBoundView.create` from hook-returned nodes are
|
|
5
|
+
* optimistic (SAFE-MODE prop regime) and revalidate affected shapes, instead of
|
|
6
|
+
* the raw kernel-client floor that bypasses the overlay. Keeps the bind-time
|
|
7
|
+
* `id → tree-path` index the write capability addresses nodes through.
|
|
8
|
+
*/
|
|
9
|
+
import type { CredentialInput } from '@astrale-os/kernel-client'
|
|
10
|
+
import type { PathLike, Properties } from '@astrale-os/kernel-client/graph'
|
|
11
|
+
import type { GraphMemory } from '@astrale-os/kernel-client/store'
|
|
12
|
+
|
|
13
|
+
import type { SessionView } from '../auth/act-as.context'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* One memory-backed writable kernel per (effective) session view, plus the
|
|
17
|
+
* bind-time `id → tree-path` index it consults. Shared across every schema binding
|
|
18
|
+
* over the same session so a node bound under any schema resolves for a write.
|
|
19
|
+
*/
|
|
20
|
+
export interface WritableKernel {
|
|
21
|
+
readonly writable: SessionView
|
|
22
|
+
readonly pathIndex: Map<string, string>
|
|
23
|
+
}
|
|
24
|
+
const writableCache = new WeakMap<object, WritableKernel>()
|
|
25
|
+
|
|
26
|
+
export function writableFor(kernel: SessionView, memory: GraphMemory): WritableKernel {
|
|
27
|
+
let entry = writableCache.get(kernel)
|
|
28
|
+
if (entry === undefined) {
|
|
29
|
+
const pathIndex = new Map<string, string>()
|
|
30
|
+
entry = { writable: withMemoryWrites(kernel, memory, pathIndex), pathIndex }
|
|
31
|
+
writableCache.set(kernel, entry)
|
|
32
|
+
}
|
|
33
|
+
return entry
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wrap a session view so `updateNode`/`createNode` route through the memory's
|
|
38
|
+
* optimistic write shorthands. `BoundNode.update` addresses a node by `@<id>`,
|
|
39
|
+
* but the memory indexes entities by tree path — so an `@<id>` ref is translated
|
|
40
|
+
* through `pathIndex` (populated at bind time). A ref that is NOT indexed (a node
|
|
41
|
+
* that was never read through a hook) falls back to the raw kernel-client floor:
|
|
42
|
+
* it still dispatches, just without the overlay — matching the imperative floor.
|
|
43
|
+
*/
|
|
44
|
+
function withMemoryWrites(
|
|
45
|
+
kernel: SessionView,
|
|
46
|
+
memory: GraphMemory,
|
|
47
|
+
pathIndex: Map<string, string>,
|
|
48
|
+
): SessionView {
|
|
49
|
+
const base = kernel as unknown as {
|
|
50
|
+
updateNode(cls: PathLike, ref: PathLike, props: Properties): Promise<void>
|
|
51
|
+
createNode(cls: PathLike, at: PathLike, props?: Properties): Promise<string>
|
|
52
|
+
as(cred: CredentialInput | (() => CredentialInput)): SessionView
|
|
53
|
+
}
|
|
54
|
+
const rawUpdate = base.updateNode.bind(base)
|
|
55
|
+
return {
|
|
56
|
+
...(kernel as object),
|
|
57
|
+
updateNode: (cls: PathLike, ref: PathLike, props: Properties): Promise<void> => {
|
|
58
|
+
const path = treePathFor(pathIndex, ref)
|
|
59
|
+
return path !== null ? memory.update(path, props) : rawUpdate(cls, ref, props)
|
|
60
|
+
},
|
|
61
|
+
createNode: (cls: PathLike, at: PathLike, props?: Properties): Promise<string> =>
|
|
62
|
+
memory.create(cls, at, props),
|
|
63
|
+
// Credential rebind stays on the raw kernel-client floor (§6 note).
|
|
64
|
+
as: (cred: CredentialInput | (() => CredentialInput)): SessionView => base.as(cred),
|
|
65
|
+
} as unknown as SessionView
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The tree path an `@<id>` write ref maps to, via the bind-time index; else null. */
|
|
69
|
+
function treePathFor(pathIndex: Map<string, string>, ref: PathLike): string | null {
|
|
70
|
+
if (typeof ref !== 'string' || !ref.startsWith('@')) return null
|
|
71
|
+
return pathIndex.get(ref.slice(1)) ?? null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Record a bound node's `id → tree path` so the write cap can address it. */
|
|
75
|
+
export function recordPath(pathIndex: Map<string, string>, node: unknown): void {
|
|
76
|
+
const n = node as { id?: unknown; path?: { raw?: unknown } } | null
|
|
77
|
+
if (n !== null && typeof n.id === 'string' && typeof n.path?.raw === 'string') {
|
|
78
|
+
pathIndex.set(n.id, n.path.raw)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// selection — API §9. SelectionScope + useSelection: an insertion-ordered id set
|
|
2
|
+
// with an anchor, projected to LIVE memory nodes.
|
|
3
|
+
export { SelectionScope } from './scope.context'
|
|
4
|
+
export type { Selection, SelectionScopeProps } from './scope.context'
|
|
5
|
+
export { useSelection } from './selection.hook'
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { GraphMemory, ReadOptions, ShapeInput } from '@astrale-os/kernel-client/store'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Selection liveness (API §9) — resolve the scope's ids to LIVE memory nodes, and
|
|
5
|
+
* the node types the selection speaks. Each id is subscribed as an independent
|
|
6
|
+
* point-read shape; an id drops ONLY once its shape has SETTLED to absence (the
|
|
7
|
+
* fixed latch) — a cold shape whose point-read has not landed is kept alive with
|
|
8
|
+
* no node, so a selection made from a children row is never self-cleared before
|
|
9
|
+
* its fetch resolves. Degrades to no nodes (ids kept) when no memory is in scope.
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useContext, useRef, useSyncExternalStore } from 'react'
|
|
12
|
+
|
|
13
|
+
import { GraphContext } from '../graph/graph.context'
|
|
14
|
+
|
|
15
|
+
/** A live memory node — the non-null projection of a point read. */
|
|
16
|
+
export type SelectionNode = NonNullable<ReturnType<GraphMemory['getSnapshot']>['node']>
|
|
17
|
+
export type NodeId = SelectionNode['id']
|
|
18
|
+
|
|
19
|
+
const LIVE_OPTS: ReadOptions = { live: true }
|
|
20
|
+
const EMPTY_NODES: readonly SelectionNode[] = []
|
|
21
|
+
const NOOP = (): void => {}
|
|
22
|
+
|
|
23
|
+
/** Point-read shape addressing a node by its graph id. */
|
|
24
|
+
function nodeShape(id: NodeId): ShapeInput {
|
|
25
|
+
return `@${id}`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A point-read snapshot — the arms selection reads (widened to tolerate fakes). */
|
|
29
|
+
type NodeSnapshot = { readonly node: SelectionNode | null; live?: boolean; pending?: boolean }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A null-node id is a CONFIRMED absence (drop it) only when the shape settled
|
|
33
|
+
* there: it was seen warm and is now gone (deletion), or a live subscription has
|
|
34
|
+
* finished fetching and found nothing (mask/never-existed). A never-warm, not-
|
|
35
|
+
* yet-fetched (cold/pending) shape is NOT absence — the id stays alive, no node.
|
|
36
|
+
*/
|
|
37
|
+
function isConfirmedAbsent(snap: NodeSnapshot, id: NodeId, everWarm: ReadonlySet<NodeId>): boolean {
|
|
38
|
+
if (everWarm.has(id)) return true
|
|
39
|
+
return snap.live === true && snap.pending === false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The in-scope graph memory, structurally narrowed. Reads the base memory the
|
|
44
|
+
* provider seats in `GraphContext`, defensively so selection also works under a
|
|
45
|
+
* bare/fake context (tests) or before one is mounted.
|
|
46
|
+
*/
|
|
47
|
+
export function useMemory(): GraphMemory | null {
|
|
48
|
+
const ctx = useContext(GraphContext) as unknown as GraphMemory | null
|
|
49
|
+
return ctx !== null &&
|
|
50
|
+
typeof ctx.subscribe === 'function' &&
|
|
51
|
+
typeof ctx.getSnapshot === 'function'
|
|
52
|
+
? ctx
|
|
53
|
+
: null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface LiveSnapshot {
|
|
57
|
+
readonly aliveIds: readonly NodeId[]
|
|
58
|
+
readonly nodes: readonly SelectionNode[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function unchanged(
|
|
62
|
+
prev: LiveSnapshot,
|
|
63
|
+
aliveIds: readonly NodeId[],
|
|
64
|
+
nodes: readonly SelectionNode[],
|
|
65
|
+
): boolean {
|
|
66
|
+
// `aliveIds` and `nodes` can differ in length (a cold id is alive with no node
|
|
67
|
+
// yet), so compare the two lists independently.
|
|
68
|
+
if (prev.aliveIds.length !== aliveIds.length || prev.nodes.length !== nodes.length) return false
|
|
69
|
+
for (let i = 0; i < aliveIds.length; i += 1) if (prev.aliveIds[i] !== aliveIds[i]) return false
|
|
70
|
+
for (let i = 0; i < nodes.length; i += 1) if (prev.nodes[i] !== nodes[i]) return false
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Subscribe to each id's node shape and project the surviving nodes. The result
|
|
76
|
+
* keeps a stable reference across renders when membership and node identities
|
|
77
|
+
* are unchanged (useSyncExternalStore contract).
|
|
78
|
+
*/
|
|
79
|
+
export function useLiveNodes(
|
|
80
|
+
orderedIds: readonly NodeId[],
|
|
81
|
+
memory: GraphMemory | null,
|
|
82
|
+
): LiveSnapshot {
|
|
83
|
+
// Stands in for `orderedIds` in the callback deps (a fresh array each render).
|
|
84
|
+
const idsKey = orderedIds.join(' ')
|
|
85
|
+
const cache = useRef<{ memory: GraphMemory | null; snap: LiveSnapshot } | null>(null)
|
|
86
|
+
// Ids whose shape has been seen warm at least once — a later null is a deletion
|
|
87
|
+
// (confirmed absence), whereas a never-warm null is merely cold. Monotonic, so
|
|
88
|
+
// re-reading getSnapshot within a render is idempotent (useSyncExternalStore-safe).
|
|
89
|
+
const everWarm = useRef<Set<NodeId>>(new Set())
|
|
90
|
+
|
|
91
|
+
const getSnapshot = useCallback((): LiveSnapshot => {
|
|
92
|
+
let aliveIds: readonly NodeId[]
|
|
93
|
+
let nodes: readonly SelectionNode[]
|
|
94
|
+
if (memory === null) {
|
|
95
|
+
// No liveness source: keep the ids, resolve no nodes.
|
|
96
|
+
aliveIds = orderedIds
|
|
97
|
+
nodes = EMPTY_NODES
|
|
98
|
+
} else {
|
|
99
|
+
const seen = everWarm.current
|
|
100
|
+
const a: NodeId[] = []
|
|
101
|
+
const n: SelectionNode[] = []
|
|
102
|
+
for (const id of orderedIds) {
|
|
103
|
+
const snap = memory.getSnapshot(nodeShape(id), LIVE_OPTS) as NodeSnapshot
|
|
104
|
+
const node = snap.node
|
|
105
|
+
if (node !== null) {
|
|
106
|
+
seen.add(id)
|
|
107
|
+
a.push(id)
|
|
108
|
+
n.push(node)
|
|
109
|
+
} else if (!isConfirmedAbsent(snap, id, seen)) {
|
|
110
|
+
// Cold / not-yet-fetched: keep the id alive, no node yet — never a
|
|
111
|
+
// premature drop (the critical self-clear this guards against).
|
|
112
|
+
a.push(id)
|
|
113
|
+
}
|
|
114
|
+
// else: settled absence (deleted/masked) → drops from ids and nodes.
|
|
115
|
+
}
|
|
116
|
+
aliveIds = a
|
|
117
|
+
nodes = n
|
|
118
|
+
}
|
|
119
|
+
const prev = cache.current
|
|
120
|
+
if (prev !== null && prev.memory === memory && unchanged(prev.snap, aliveIds, nodes)) {
|
|
121
|
+
return prev.snap
|
|
122
|
+
}
|
|
123
|
+
const snap: LiveSnapshot = { aliveIds, nodes }
|
|
124
|
+
cache.current = { memory, snap }
|
|
125
|
+
return snap
|
|
126
|
+
}, [memory, idsKey])
|
|
127
|
+
|
|
128
|
+
const subscribe = useCallback(
|
|
129
|
+
(onStoreChange: () => void): (() => void) => {
|
|
130
|
+
if (memory === null || orderedIds.length === 0) return NOOP
|
|
131
|
+
const unsubs = orderedIds.map((id) =>
|
|
132
|
+
memory.subscribe(nodeShape(id), onStoreChange, LIVE_OPTS),
|
|
133
|
+
)
|
|
134
|
+
return () => {
|
|
135
|
+
for (const unsub of unsubs) unsub()
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
[memory, idsKey],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
142
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SelectionScope (API §9) — an insertion-ordered id set with an anchor (the last
|
|
5
|
+
* explicit pick), projected to LIVE memory nodes. A deleted node drops out of
|
|
6
|
+
* `ids`/`nodes`, and the scope re-canonicalizes its stored set to match. Range
|
|
7
|
+
* math stays caller-side (read `anchor`). Seats the selection context.
|
|
8
|
+
*/
|
|
9
|
+
import { createContext, useCallback, useEffect, useMemo, useReducer, useRef } from 'react'
|
|
10
|
+
|
|
11
|
+
import type { NodeId, SelectionNode } from './live'
|
|
12
|
+
|
|
13
|
+
import { useLiveNodes, useMemory } from './live'
|
|
14
|
+
|
|
15
|
+
/** The value `useSelection()` returns (API §9). */
|
|
16
|
+
export interface Selection {
|
|
17
|
+
/** Selected ids, insertion-ordered. A deleted node drops out. */
|
|
18
|
+
readonly ids: readonly NodeId[]
|
|
19
|
+
/** LIVE nodes for `ids`, same order — a deleted node drops out here too. */
|
|
20
|
+
readonly nodes: readonly SelectionNode[]
|
|
21
|
+
/** Last explicit pick; null when empty or the anchor node is gone. Range math is caller-side. */
|
|
22
|
+
readonly anchor: NodeId | null
|
|
23
|
+
has(id: NodeId): boolean
|
|
24
|
+
set(ids: readonly NodeId[]): void
|
|
25
|
+
add(id: NodeId): void
|
|
26
|
+
toggle(id: NodeId): void
|
|
27
|
+
clear(): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SelectionScopeProps {
|
|
31
|
+
/** Seed selection (deduped); read once at mount. */
|
|
32
|
+
initial?: readonly NodeId[]
|
|
33
|
+
/** Fires when the selection changes — explicit ops AND liveness drops. Persist here. */
|
|
34
|
+
onChange?: (ids: readonly NodeId[]) => void
|
|
35
|
+
children: ReactNode
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const SelectionContext = createContext<Selection | null>(null)
|
|
39
|
+
SelectionContext.displayName = 'AstraleSelectionContext'
|
|
40
|
+
|
|
41
|
+
interface State {
|
|
42
|
+
readonly ids: readonly NodeId[]
|
|
43
|
+
readonly anchor: NodeId | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Action =
|
|
47
|
+
| { readonly t: 'set'; readonly ids: readonly NodeId[] }
|
|
48
|
+
| { readonly t: 'add'; readonly id: NodeId }
|
|
49
|
+
| { readonly t: 'toggle'; readonly id: NodeId }
|
|
50
|
+
| { readonly t: 'clear' }
|
|
51
|
+
| { readonly t: 'prune'; readonly alive: readonly NodeId[] }
|
|
52
|
+
|
|
53
|
+
function dedupe(ids: readonly NodeId[]): readonly NodeId[] {
|
|
54
|
+
const seen = new Set<NodeId>()
|
|
55
|
+
const out: NodeId[] = []
|
|
56
|
+
for (const id of ids) {
|
|
57
|
+
if (!seen.has(id)) {
|
|
58
|
+
seen.add(id)
|
|
59
|
+
out.push(id)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return out
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function sameIds(a: readonly NodeId[], b: readonly NodeId[]): boolean {
|
|
66
|
+
if (a.length !== b.length) return false
|
|
67
|
+
for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function reducer(state: State, action: Action): State {
|
|
72
|
+
switch (action.t) {
|
|
73
|
+
case 'set': {
|
|
74
|
+
const ids = dedupe(action.ids)
|
|
75
|
+
const anchor = ids.length > 0 ? ids[ids.length - 1]! : null
|
|
76
|
+
if (sameIds(state.ids, ids) && state.anchor === anchor) return state
|
|
77
|
+
return { ids, anchor }
|
|
78
|
+
}
|
|
79
|
+
case 'add': {
|
|
80
|
+
if (state.ids.includes(action.id)) {
|
|
81
|
+
return state.anchor === action.id ? state : { ids: state.ids, anchor: action.id }
|
|
82
|
+
}
|
|
83
|
+
return { ids: [...state.ids, action.id], anchor: action.id }
|
|
84
|
+
}
|
|
85
|
+
case 'toggle': {
|
|
86
|
+
// Toggle-off keeps the anchor (the removed id is no longer a valid pivot).
|
|
87
|
+
if (state.ids.includes(action.id)) {
|
|
88
|
+
return { ids: state.ids.filter((x) => x !== action.id), anchor: state.anchor }
|
|
89
|
+
}
|
|
90
|
+
return { ids: [...state.ids, action.id], anchor: action.id }
|
|
91
|
+
}
|
|
92
|
+
case 'clear':
|
|
93
|
+
return state.ids.length === 0 && state.anchor === null ? state : { ids: [], anchor: null }
|
|
94
|
+
case 'prune': {
|
|
95
|
+
const anchorAlive = state.anchor === null || action.alive.includes(state.anchor)
|
|
96
|
+
if (sameIds(state.ids, action.alive)) {
|
|
97
|
+
return anchorAlive ? state : { ids: state.ids, anchor: null }
|
|
98
|
+
}
|
|
99
|
+
return { ids: action.alive, anchor: anchorAlive ? state.anchor : null }
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function initState(initial: readonly NodeId[] | undefined): State {
|
|
105
|
+
return { ids: dedupe(initial ?? []), anchor: null }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function SelectionScope(props: SelectionScopeProps): ReactElement {
|
|
109
|
+
const { initial, onChange, children } = props
|
|
110
|
+
const [state, dispatch] = useReducer(reducer, initial, initState)
|
|
111
|
+
|
|
112
|
+
const memory = useMemory()
|
|
113
|
+
const { aliveIds, nodes } = useLiveNodes(state.ids, memory)
|
|
114
|
+
|
|
115
|
+
// Re-canonicalize stored state when liveness drops a node (or its anchor).
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const anchorDead = state.anchor !== null && !aliveIds.includes(state.anchor)
|
|
118
|
+
if (!sameIds(state.ids, aliveIds) || anchorDead) dispatch({ t: 'prune', alive: aliveIds })
|
|
119
|
+
}, [aliveIds, state.ids, state.anchor])
|
|
120
|
+
|
|
121
|
+
// Report changes (explicit ops AND liveness drops), skipping the initial mount.
|
|
122
|
+
const onChangeRef = useRef(onChange)
|
|
123
|
+
onChangeRef.current = onChange
|
|
124
|
+
const mounted = useRef(false)
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!mounted.current) {
|
|
127
|
+
mounted.current = true
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
onChangeRef.current?.(aliveIds)
|
|
131
|
+
}, [aliveIds])
|
|
132
|
+
|
|
133
|
+
const set = useCallback((ids: readonly NodeId[]) => dispatch({ t: 'set', ids }), [])
|
|
134
|
+
const add = useCallback((id: NodeId) => dispatch({ t: 'add', id }), [])
|
|
135
|
+
const toggle = useCallback((id: NodeId) => dispatch({ t: 'toggle', id }), [])
|
|
136
|
+
const clear = useCallback(() => dispatch({ t: 'clear' }), [])
|
|
137
|
+
|
|
138
|
+
const aliveSet = useMemo(() => new Set(aliveIds), [aliveIds])
|
|
139
|
+
const aliveSetRef = useRef(aliveSet)
|
|
140
|
+
aliveSetRef.current = aliveSet
|
|
141
|
+
const has = useCallback((id: NodeId) => aliveSetRef.current.has(id), [])
|
|
142
|
+
|
|
143
|
+
const anchor = state.anchor !== null && aliveSet.has(state.anchor) ? state.anchor : null
|
|
144
|
+
|
|
145
|
+
const value = useMemo<Selection>(
|
|
146
|
+
() => ({ ids: aliveIds, nodes, anchor, has, set, add, toggle, clear }),
|
|
147
|
+
[aliveIds, nodes, anchor, has, set, add, toggle, clear],
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
return <SelectionContext.Provider value={value}>{children}</SelectionContext.Provider>
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useSelection (API §9) — the nearest selection scope's value (ids/nodes/anchor +
|
|
3
|
+
* has/set/add/toggle/clear). Throws outside a `<SelectionScope>`.
|
|
4
|
+
*/
|
|
5
|
+
import { useContext } from 'react'
|
|
6
|
+
|
|
7
|
+
import type { Selection } from './scope.context'
|
|
8
|
+
|
|
9
|
+
import { SelectionContext } from './scope.context'
|
|
10
|
+
|
|
11
|
+
/** The nearest selection scope. Throws outside a `<SelectionScope>`. */
|
|
12
|
+
export function useSelection(): Selection {
|
|
13
|
+
const selection = useContext(SelectionContext)
|
|
14
|
+
if (selection === null) {
|
|
15
|
+
throw new Error('useSelection must be used within a <SelectionScope>')
|
|
16
|
+
}
|
|
17
|
+
return selection
|
|
18
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boot machine inputs (API §2): config identity (what triggers a reboot) and
|
|
3
|
+
* whoami resolution (`createShell → init → whoami → OS-user preresolve`). Kept
|
|
4
|
+
* pure of React so the provider's boot effect composes them.
|
|
5
|
+
*/
|
|
6
|
+
import type { Shell, ShellConfig } from '@astrale-os/shell'
|
|
7
|
+
|
|
8
|
+
import type { SelfNode } from '../session.context'
|
|
9
|
+
|
|
10
|
+
export function isShellConfig(config: ShellConfig | Shell): config is ShellConfig {
|
|
11
|
+
return 'mode' in config
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let prebuiltCounter = 0
|
|
15
|
+
const prebuiltIds = new WeakMap<object, string>()
|
|
16
|
+
/** Stable id per prebuilt-Shell object reference (a new object reboots). */
|
|
17
|
+
function prebuiltId(shell: object): string {
|
|
18
|
+
let id = prebuiltIds.get(shell)
|
|
19
|
+
if (id === undefined) {
|
|
20
|
+
prebuiltCounter += 1
|
|
21
|
+
id = `prebuilt#${prebuiltCounter}`
|
|
22
|
+
prebuiltIds.set(shell, id)
|
|
23
|
+
}
|
|
24
|
+
return id
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Identity of a config — primitive fields only, so re-passing an inline `fetch`,
|
|
29
|
+
* `adapter`, `getCredential`, or `parentWindow` each render does not reboot;
|
|
30
|
+
* changing the kernel/credential does. A principal switch driven by a NEW
|
|
31
|
+
* `getCredential` closure (rather than a new `credential` string) is therefore
|
|
32
|
+
* NOT observed here — remount the provider (React `key`) to rebind it.
|
|
33
|
+
*/
|
|
34
|
+
export function configIdentity(config: ShellConfig | Shell): string {
|
|
35
|
+
if (!isShellConfig(config)) return prebuiltId(config)
|
|
36
|
+
if (config.mode === 'standalone') {
|
|
37
|
+
return [
|
|
38
|
+
'standalone',
|
|
39
|
+
config.kernelUrl,
|
|
40
|
+
config.credential ?? '',
|
|
41
|
+
config.mintIdentity ?? '',
|
|
42
|
+
config.windowId ?? '',
|
|
43
|
+
].join('|')
|
|
44
|
+
}
|
|
45
|
+
return `sandboxed|${config.initTimeoutMs ?? ''}`
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve whoami when the bound kernel exposes an auth surface; else null. */
|
|
49
|
+
export async function resolveSelf(shell: Shell): Promise<SelfNode | null> {
|
|
50
|
+
const kernel = shell.kernel as unknown as {
|
|
51
|
+
auth?: { whoami?: () => Promise<SelfNode> }
|
|
52
|
+
}
|
|
53
|
+
const auth = kernel.auth
|
|
54
|
+
const whoami = auth?.whoami
|
|
55
|
+
if (typeof whoami !== 'function') return null
|
|
56
|
+
return whoami.call(auth)
|
|
57
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StrictMode/HMR single-ownership + deferred teardown (API §2). A booted shell's
|
|
3
|
+
* dispose is deferred one macrotask keyed by the SHELL object, so a synchronous
|
|
4
|
+
* re-mount (StrictMode/HMR, or a different provider reusing a prebuilt Shell)
|
|
5
|
+
* adopts the same shell by cancelling the pending dispose before it fires.
|
|
6
|
+
*/
|
|
7
|
+
import type { Shell } from '@astrale-os/shell'
|
|
8
|
+
|
|
9
|
+
export interface BootRef {
|
|
10
|
+
key: string | null
|
|
11
|
+
shell: Shell | null
|
|
12
|
+
/** Bumped per boot attempt; an in-flight init/whoami whose generation is stale is ignored. */
|
|
13
|
+
generation: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Pending deferred teardowns keyed by the SHELL object (not the provider): a new
|
|
18
|
+
* adopter of the same shell — a StrictMode/HMR re-setup, OR a different provider
|
|
19
|
+
* instance reusing a prebuilt Shell — cancels the pending dispose before it
|
|
20
|
+
* fires, so the old provider's timer can never dispose a shell the new one just
|
|
21
|
+
* adopted. Config-built shells are unique per boot, so they never collide here.
|
|
22
|
+
*/
|
|
23
|
+
const pendingTeardowns = new WeakMap<Shell, ReturnType<typeof setTimeout>>()
|
|
24
|
+
|
|
25
|
+
export function cancelTeardown(shell: Shell): void {
|
|
26
|
+
const timer = pendingTeardowns.get(shell)
|
|
27
|
+
if (timer !== undefined) {
|
|
28
|
+
clearTimeout(timer)
|
|
29
|
+
pendingTeardowns.delete(shell)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Defer a shell's dispose one macrotask, resetting the owning ref when it fires. */
|
|
34
|
+
export function scheduleTeardown(shell: Shell, ref: BootRef): void {
|
|
35
|
+
cancelTeardown(shell)
|
|
36
|
+
const timer = setTimeout(() => {
|
|
37
|
+
pendingTeardowns.delete(shell)
|
|
38
|
+
ref.generation += 1
|
|
39
|
+
disposeShell(shell)
|
|
40
|
+
if (ref.shell === shell) ref.shell = null
|
|
41
|
+
ref.key = null
|
|
42
|
+
}, 0)
|
|
43
|
+
pendingTeardowns.set(shell, timer)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function disposeShell(shell: Shell): void {
|
|
47
|
+
// Best-effort: dispose never rejects meaningfully for a booted shell.
|
|
48
|
+
try {
|
|
49
|
+
const result = shell.dispose() as unknown
|
|
50
|
+
if (result instanceof Promise) result.catch(() => undefined)
|
|
51
|
+
} catch {
|
|
52
|
+
/* swallow — teardown must not throw */
|
|
53
|
+
}
|
|
54
|
+
}
|