@astrale-os/shell-react 0.4.0-beta.14 → 0.4.0-beta.15
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/CHANGELOG.md +19 -0
- package/dist/application/apps/app-host.d.ts +2 -2
- package/dist/application/apps/app-host.js +1 -1
- package/dist/application/apps/open-app.hook.d.ts +2 -2
- package/dist/application/apps/open-app.hook.js +1 -1
- package/dist/application/index.d.ts +1 -9
- package/dist/application/index.js +0 -8
- package/dist/application/user/user.hook.d.ts +2 -2
- package/dist/application/user/user.store.d.ts +2 -4
- package/dist/application/user/user.store.js +1 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/schema/action.hook.d.ts +5 -5
- package/dist/schema/action.hook.js +6 -2
- package/package.json +4 -4
- package/src/application/apps/app-host.tsx +3 -3
- package/src/application/apps/open-app.hook.ts +3 -3
- package/src/application/index.ts +1 -13
- package/src/application/user/user.hook.ts +2 -2
- package/src/application/user/user.store.ts +3 -14
- package/src/index.ts +2 -21
- package/src/schema/action.hook.ts +11 -7
- package/dist/application/apps/apps.hook.d.ts +0 -7
- package/dist/application/apps/apps.hook.js +0 -19
- package/dist/application/spaces/current-space.hook.d.ts +0 -6
- package/dist/application/spaces/current-space.hook.js +0 -27
- package/dist/application/spaces/current-space.store.d.ts +0 -31
- package/dist/application/spaces/current-space.store.js +0 -79
- package/dist/application/spaces/space.hook.d.ts +0 -4
- package/dist/application/spaces/space.hook.js +0 -28
- package/dist/application/spaces/spaces.hook.d.ts +0 -12
- package/dist/application/spaces/spaces.hook.js +0 -89
- package/src/application/apps/apps.hook.ts +0 -38
- package/src/application/spaces/current-space.hook.ts +0 -43
- package/src/application/spaces/current-space.store.ts +0 -110
- package/src/application/spaces/space.hook.ts +0 -45
- package/src/application/spaces/spaces.hook.ts +0 -118
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0-beta.15](https://github.com/astrale-os/shell/compare/shell-react-v0.4.0-beta.14...shell-react-v0.4.0-beta.15) (2026-09-07)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### ⚠ BREAKING CHANGES
|
|
7
|
+
|
|
8
|
+
* **react:** preflight exact-class instance method targets ([#182](https://github.com/astrale-os/shell/issues/182))
|
|
9
|
+
* **runtime:** simplify Shell users, groups and invitations ([#187](https://github.com/astrale-os/shell/issues/187))
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* **domain:** migrate Shell to project environments ([#183](https://github.com/astrale-os/shell/issues/183)) ([ec72b5d](https://github.com/astrale-os/shell/commit/ec72b5d11fea5ff3266d1fd0a823c4eff5cc6d3e))
|
|
14
|
+
* **react:** preflight exact-class instance method targets ([#182](https://github.com/astrale-os/shell/issues/182)) ([305e92f](https://github.com/astrale-os/shell/commit/305e92f0578309e5e94d8fd78d874f7c5a60ee46))
|
|
15
|
+
* **runtime:** simplify Shell users, groups and invitations ([#187](https://github.com/astrale-os/shell/issues/187)) ([7f00279](https://github.com/astrale-os/shell/commit/7f002791edae81944e6e80940834dee49212aa26))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Bug Fixes
|
|
19
|
+
|
|
20
|
+
* **react:** require the Shell User and Group API release ([#196](https://github.com/astrale-os/shell/issues/196)) ([df34749](https://github.com/astrale-os/shell/commit/df3474972a54c30816a02fc3510b478299bea4f1))
|
|
21
|
+
|
|
3
22
|
## [0.4.0-beta.14](https://github.com/astrale-os/shell/compare/shell-react-v0.4.0-beta.13...shell-react-v0.4.0-beta.14) (2026-09-04)
|
|
4
23
|
|
|
5
24
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Key } from '@astrale-os/sdk/schema';
|
|
2
|
-
import type {
|
|
2
|
+
import type { HostCapabilities, ResolvedView } from '@astrale-os/shell';
|
|
3
3
|
import type { CSSProperties, ReactNode } from 'react';
|
|
4
4
|
import type { ViewHostHandle } from '../../window/host.js';
|
|
5
5
|
import type { WindowState } from '../../window/lifecycle/states.js';
|
|
@@ -9,7 +9,7 @@ export type AppHostFallbackState = WindowState | {
|
|
|
9
9
|
readonly phase: 'no-view';
|
|
10
10
|
};
|
|
11
11
|
export interface AppHostProps {
|
|
12
|
-
app:
|
|
12
|
+
app: Key.Origin;
|
|
13
13
|
/** Override the installed Domain entry View. */
|
|
14
14
|
view?: ResolvedView;
|
|
15
15
|
capabilities?: HostCapabilities;
|
|
@@ -7,7 +7,7 @@ export const AppHost = forwardRef(function AppHost(props, ref) {
|
|
|
7
7
|
const open = useOpenApp();
|
|
8
8
|
const [opened, setOpened] = useState(undefined);
|
|
9
9
|
const [error, setError] = useState(undefined);
|
|
10
|
-
const origin =
|
|
10
|
+
const origin = props.app;
|
|
11
11
|
const openRef = useRef(open);
|
|
12
12
|
openRef.current = open;
|
|
13
13
|
useEffect(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Key } from '@astrale-os/sdk/schema';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResolvedView } from '@astrale-os/shell';
|
|
3
3
|
import type { Action } from '../../graph/action.js';
|
|
4
4
|
/** Resolve one installed Domain's ordinary entry View. */
|
|
5
|
-
export declare function useOpenApp(): Action<[
|
|
5
|
+
export declare function useOpenApp(): Action<[Key.Origin], ResolvedView | null>;
|
|
@@ -3,5 +3,5 @@ import { useShell } from '../../session/shell.hook.js';
|
|
|
3
3
|
/** Resolve one installed Domain's ordinary entry View. */
|
|
4
4
|
export function useOpenApp() {
|
|
5
5
|
const shell = useShell();
|
|
6
|
-
return useAction((
|
|
6
|
+
return useAction((origin) => shell.views.entrypoint(origin));
|
|
7
7
|
}
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
export { useUser } from './user/user.hook.js';
|
|
2
|
-
export { useSpaces } from './spaces/spaces.hook.js';
|
|
3
|
-
export type { SpacesResult } from './spaces/spaces.hook.js';
|
|
4
|
-
export { useCurrentSpace } from './spaces/current-space.hook.js';
|
|
5
|
-
export type { CurrentSpaceResult } from './spaces/current-space.hook.js';
|
|
6
|
-
export { useSpace } from './spaces/space.hook.js';
|
|
7
|
-
export type { SpaceResult } from './spaces/space.hook.js';
|
|
8
|
-
export { useApps } from './apps/apps.hook.js';
|
|
9
|
-
export type { AppsResult } from './apps/apps.hook.js';
|
|
10
2
|
export { useOpenApp } from './apps/open-app.hook.js';
|
|
11
3
|
export { AppHost } from './apps/app-host.js';
|
|
12
4
|
export type { AppHostFallbackState, AppHostProps } from './apps/app-host.js';
|
|
13
5
|
export { AppLogo } from './apps/app-logo.js';
|
|
14
6
|
export type { AppLogoProps } from './apps/app-logo.js';
|
|
15
|
-
export type {
|
|
7
|
+
export type { NodeTarget, ResolvedView, UserNode } from '@astrale-os/shell';
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
// application — API §12.2. The OS-plane hooks over the shell package's
|
|
2
|
-
// `shell.user/spaces/apps` namespaces + the window module: useUser, useSpaces,
|
|
3
|
-
// useCurrentSpace, useSpace, useApps, useOpenApp,
|
|
4
|
-
// AppHost, AppLogo.
|
|
5
1
|
export { useUser } from './user/user.hook.js';
|
|
6
|
-
export { useSpaces } from './spaces/spaces.hook.js';
|
|
7
|
-
export { useCurrentSpace } from './spaces/current-space.hook.js';
|
|
8
|
-
export { useSpace } from './spaces/space.hook.js';
|
|
9
|
-
export { useApps } from './apps/apps.hook.js';
|
|
10
2
|
export { useOpenApp } from './apps/open-app.hook.js';
|
|
11
3
|
export { AppHost } from './apps/app-host.js';
|
|
12
4
|
export { AppLogo } from './apps/app-logo.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useUser
|
|
3
|
-
*
|
|
2
|
+
* useUser — self as a Shell.User descendant, preserving canonical graph property
|
|
3
|
+
* keys. The provider PRE-RESOLVES it at boot (`primeUser`),
|
|
4
4
|
* so `useUser` is synchronous below the provider (API §2). OS-plane absent → throws
|
|
5
5
|
* descriptively to the nearest boundary; a never-primed shell suspends on first use
|
|
6
6
|
* (§1.4). FLAG: not yet re-rendered on rename — becomes live via a memory shape.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The OS-plane user store (API §2/§12.2). Provider-boot priming (`primeUser`) seeds
|
|
3
3
|
* the per-Shell user cache so `useUser` is synchronous, and tracks OS-plane
|
|
4
|
-
* availability: a missing/rejecting `shell.user.me`
|
|
5
|
-
*
|
|
4
|
+
* availability: a missing/rejecting `shell.user.me` is retained in the User cache
|
|
5
|
+
* without failing boot. A never-primed shell falls back to
|
|
6
6
|
* a lazy cold read that SUSPENDS on first use (§1.4).
|
|
7
7
|
*/
|
|
8
8
|
import type { Shell, UserNode } from '@astrale-os/shell';
|
|
@@ -22,8 +22,6 @@ export type UserEntry = {
|
|
|
22
22
|
error: unknown;
|
|
23
23
|
};
|
|
24
24
|
export declare const userCache: WeakMap<Shell, UserEntry>;
|
|
25
|
-
/** Is the OS plane (shell.user/spaces/apps) usable for this shell? */
|
|
26
|
-
export declare function isOsPlaneAvailable(shell: Shell): boolean;
|
|
27
25
|
/**
|
|
28
26
|
* Provider-boot priming (API §2). Best-effort: seed the user cache so `useUser`
|
|
29
27
|
* is synchronous, or mark the OS plane down. NEVER throws — boot must not fail on
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
export const userCache = new WeakMap();
|
|
2
|
-
/** Shells whose OS plane could not be resolved at boot — OS hooks read idle. */
|
|
3
|
-
const osPlaneDown = new WeakSet();
|
|
4
|
-
/** Is the OS plane (shell.user/spaces/apps) usable for this shell? */
|
|
5
|
-
export function isOsPlaneAvailable(shell) {
|
|
6
|
-
return !osPlaneDown.has(shell);
|
|
7
|
-
}
|
|
8
2
|
function osPlaneError(cause) {
|
|
9
3
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
10
|
-
return new Error(`The
|
|
4
|
+
return new Error(`The Shell User is unavailable (useUser): ` +
|
|
11
5
|
`shell.user.me() could not resolve ` +
|
|
12
6
|
`— the workspace/shell domain may not be installed for this identity. (${detail})`);
|
|
13
7
|
}
|
|
@@ -19,7 +13,6 @@ function osPlaneError(cause) {
|
|
|
19
13
|
export async function primeUser(shell) {
|
|
20
14
|
const me = shell.user?.me;
|
|
21
15
|
if (typeof me !== 'function') {
|
|
22
|
-
osPlaneDown.add(shell);
|
|
23
16
|
userCache.set(shell, {
|
|
24
17
|
status: 'unavailable',
|
|
25
18
|
error: osPlaneError('shell.user.me is not a function'),
|
|
@@ -28,11 +21,9 @@ export async function primeUser(shell) {
|
|
|
28
21
|
}
|
|
29
22
|
try {
|
|
30
23
|
const value = await me.call(shell.user);
|
|
31
|
-
osPlaneDown.delete(shell);
|
|
32
24
|
userCache.set(shell, { status: 'resolved', value });
|
|
33
25
|
}
|
|
34
26
|
catch (error) {
|
|
35
|
-
osPlaneDown.add(shell);
|
|
36
27
|
userCache.set(shell, { status: 'unavailable', error: osPlaneError(error) });
|
|
37
28
|
}
|
|
38
29
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -20,12 +20,12 @@ export { useEmitIntent, useIntent, useSendIntent } from './intents/index.js';
|
|
|
20
20
|
export type { EmitIntent, IntentCtx, IntentPayload, IntentReply, SendIntent, } from './intents/index.js';
|
|
21
21
|
export { Can, ConnectionBoundary, CredentialBoundary, defaultViewError, defaultViewLoading, GraphBoundary, Astrale, ViewBoundary, ViewErrorBoundary, } from './components/index.js';
|
|
22
22
|
export type { AstraleProps, ViewErrorBoundaryProps } from './components/index.js';
|
|
23
|
-
export { AppHost, AppLogo,
|
|
24
|
-
export type { AppHostFallbackState, AppHostProps, AppLogoProps
|
|
23
|
+
export { AppHost, AppLogo, useOpenApp, useUser } from './application/index.js';
|
|
24
|
+
export type { AppHostFallbackState, AppHostProps, AppLogoProps } from './application/index.js';
|
|
25
25
|
export { NodeId } from '@astrale-os/sdk/graph/node';
|
|
26
26
|
export { NodePath, Path } from '@astrale-os/sdk/graph/path';
|
|
27
27
|
export type { Node } from '@astrale-os/sdk/graph/node';
|
|
28
28
|
export type { PathLike } from '@astrale-os/sdk/graph/path';
|
|
29
29
|
export type { AuthApi, Identity, PolicyDecisionInput } from '@astrale-os/sdk/auth';
|
|
30
30
|
export type { DomainBinding } from '@astrale-os/shell';
|
|
31
|
-
export type {
|
|
31
|
+
export type { CloseResult, CredentialSnapshot, HostCapabilities, IntentRegistry, KernelClient, MountCredential, NodeTarget, Shell, ShellConfig, UserNode, ViewLocation, ViewPresentation, ViewPresentationRequest, Window, } from '@astrale-os/shell';
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { useEmitIntent, useIntent, useSendIntent } from './intents/index.js';
|
|
|
17
17
|
// Headless composition kit.
|
|
18
18
|
export { Can, ConnectionBoundary, CredentialBoundary, defaultViewError, defaultViewLoading, GraphBoundary, Astrale, ViewBoundary, ViewErrorBoundary, } from './components/index.js';
|
|
19
19
|
// Shell application conveniences.
|
|
20
|
-
export { AppHost, AppLogo,
|
|
20
|
+
export { AppHost, AppLogo, useOpenApp, useUser } from './application/index.js';
|
|
21
21
|
// Canonical identity carriers used by View props and rich hooks.
|
|
22
22
|
export { NodeId } from '@astrale-os/sdk/graph/node';
|
|
23
23
|
export { NodePath, Path } from '@astrale-os/sdk/graph/path';
|
|
@@ -2,14 +2,14 @@ import type { SessionRequestOptions } from '@astrale-os/sdk/client/session';
|
|
|
2
2
|
import type { NodeId } from '@astrale-os/sdk/graph/node';
|
|
3
3
|
import type { QueryAST, QueryDefinition } from '@astrale-os/sdk/query';
|
|
4
4
|
import type { Domain } from '@astrale-os/sdk/schema';
|
|
5
|
-
import type { CallableInputOf, CallableResultOf,
|
|
6
|
-
type
|
|
7
|
-
type
|
|
8
|
-
type StaticMethod = ResolvedMethod<unknown, unknown, true, ExecutableInheritance>;
|
|
5
|
+
import type { CallableInputOf, CallableResultOf, ClassKey, ResolvedFunction, ResolvedMethod } from '@astrale-os/sdk/schema';
|
|
6
|
+
type InstanceMethod = ResolvedMethod<unknown, unknown, false, false>;
|
|
7
|
+
type StaticMethod = ResolvedMethod<unknown, unknown, true, false>;
|
|
9
8
|
type Callable = ResolvedFunction | InstanceMethod | StaticMethod;
|
|
10
|
-
/**
|
|
9
|
+
/** Receiver identity with optional observed Class for early rejection; the Kernel checks live Class. */
|
|
11
10
|
export interface ActionReceiver {
|
|
12
11
|
readonly id: NodeId;
|
|
12
|
+
readonly class?: ClassKey;
|
|
13
13
|
}
|
|
14
14
|
declare const QUERY_REFRESH: unique symbol;
|
|
15
15
|
export interface QueryRefresh {
|
|
@@ -41,7 +41,7 @@ export function useAction(callable, options = {}) {
|
|
|
41
41
|
const { refresh: _refresh, onError: _onError, ...request } = policy;
|
|
42
42
|
try {
|
|
43
43
|
const result = isInstanceMethod(current)
|
|
44
|
-
? await kernel.session.invoke(reference(bindingRef.current.domain, current)(receiverPath(args[0])), args[1], request)
|
|
44
|
+
? await kernel.session.invoke(reference(bindingRef.current.domain, current)(receiverPath(args[0], current)), args[1], request)
|
|
45
45
|
: await kernel.session.invoke(reference(bindingRef.current.domain, current), args[0], request);
|
|
46
46
|
const refresh = policy.refresh ?? 'all';
|
|
47
47
|
if (refresh === 'all')
|
|
@@ -80,7 +80,11 @@ function projectAction(action) {
|
|
|
80
80
|
function isInstanceMethod(callable) {
|
|
81
81
|
return 'owner' in callable && callable.static === false;
|
|
82
82
|
}
|
|
83
|
-
function receiverPath(receiver) {
|
|
83
|
+
function receiverPath(receiver, method) {
|
|
84
|
+
const observedClass = receiver.class;
|
|
85
|
+
if (observedClass !== undefined && observedClass !== method.owner.key) {
|
|
86
|
+
throw new TypeError(`Method ${method.key} requires exact receiver Class ${method.owner.key}.`);
|
|
87
|
+
}
|
|
84
88
|
return Path.id(receiver.id);
|
|
85
89
|
}
|
|
86
90
|
function queryRefreshIdentity(definition) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astrale-os/shell-react",
|
|
3
|
-
"version": "0.4.0-beta.
|
|
3
|
+
"version": "0.4.0-beta.15",
|
|
4
4
|
"description": "Astrale shell-react — the React face of the shell (provider, hooks, view components)",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"registry": "https://registry.npmjs.org"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@astrale-os/shell": ">=0.5.0-beta.
|
|
29
|
+
"@astrale-os/shell": ">=0.5.0-beta.15 <1.0.0",
|
|
30
30
|
"jose": "^6.2.8",
|
|
31
31
|
"zod": "4.4.3"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@astrale-os/sdk": "0.5.0-beta.
|
|
34
|
+
"@astrale-os/sdk": "0.5.0-beta.123",
|
|
35
35
|
"@testing-library/dom": "^10.4.0",
|
|
36
36
|
"@testing-library/react": "^16.3.0",
|
|
37
37
|
"@types/node": "^26.2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"vitest": "4.1.10"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@astrale-os/sdk": ">=0.5.0-beta.
|
|
49
|
+
"@astrale-os/sdk": ">=0.5.0-beta.123 <1.0.0",
|
|
50
50
|
"react": ">=18",
|
|
51
51
|
"react-dom": ">=18"
|
|
52
52
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Key } from '@astrale-os/sdk/schema'
|
|
2
|
-
import type {
|
|
2
|
+
import type { HostCapabilities, ResolvedView } from '@astrale-os/shell'
|
|
3
3
|
import type { CSSProperties, ReactElement, ReactNode } from 'react'
|
|
4
4
|
|
|
5
5
|
import { forwardRef, useEffect, useRef, useState } from 'react'
|
|
@@ -16,7 +16,7 @@ export type AppHostFallbackState =
|
|
|
16
16
|
| { readonly phase: 'no-view' }
|
|
17
17
|
|
|
18
18
|
export interface AppHostProps {
|
|
19
|
-
app:
|
|
19
|
+
app: Key.Origin
|
|
20
20
|
/** Override the installed Domain entry View. */
|
|
21
21
|
view?: ResolvedView
|
|
22
22
|
capabilities?: HostCapabilities
|
|
@@ -31,7 +31,7 @@ export const AppHost = forwardRef<ViewHostHandle, AppHostProps>(
|
|
|
31
31
|
const open = useOpenApp()
|
|
32
32
|
const [opened, setOpened] = useState<ResolvedView | null | undefined>(undefined)
|
|
33
33
|
const [error, setError] = useState<unknown>(undefined)
|
|
34
|
-
const origin =
|
|
34
|
+
const origin = props.app
|
|
35
35
|
const openRef = useRef(open)
|
|
36
36
|
openRef.current = open
|
|
37
37
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Key } from '@astrale-os/sdk/schema'
|
|
2
|
-
import type {
|
|
2
|
+
import type { ResolvedView } from '@astrale-os/shell'
|
|
3
3
|
|
|
4
4
|
import type { Action } from '../../graph/action.js'
|
|
5
5
|
|
|
6
6
|
import { useAction } from '../../graph/action.js'
|
|
7
7
|
import { useShell } from '../../session/shell.hook.js'
|
|
8
8
|
/** Resolve one installed Domain's ordinary entry View. */
|
|
9
|
-
export function useOpenApp(): Action<[
|
|
9
|
+
export function useOpenApp(): Action<[Key.Origin], ResolvedView | null> {
|
|
10
10
|
const shell = useShell()
|
|
11
|
-
return useAction((
|
|
11
|
+
return useAction((origin: Key.Origin) => shell.views.entrypoint(origin))
|
|
12
12
|
}
|
package/src/application/index.ts
CHANGED
|
@@ -1,20 +1,8 @@
|
|
|
1
|
-
// application — API §12.2. The OS-plane hooks over the shell package's
|
|
2
|
-
// `shell.user/spaces/apps` namespaces + the window module: useUser, useSpaces,
|
|
3
|
-
// useCurrentSpace, useSpace, useApps, useOpenApp,
|
|
4
|
-
// AppHost, AppLogo.
|
|
5
1
|
export { useUser } from './user/user.hook.js'
|
|
6
|
-
export { useSpaces } from './spaces/spaces.hook.js'
|
|
7
|
-
export type { SpacesResult } from './spaces/spaces.hook.js'
|
|
8
|
-
export { useCurrentSpace } from './spaces/current-space.hook.js'
|
|
9
|
-
export type { CurrentSpaceResult } from './spaces/current-space.hook.js'
|
|
10
|
-
export { useSpace } from './spaces/space.hook.js'
|
|
11
|
-
export type { SpaceResult } from './spaces/space.hook.js'
|
|
12
|
-
export { useApps } from './apps/apps.hook.js'
|
|
13
|
-
export type { AppsResult } from './apps/apps.hook.js'
|
|
14
2
|
export { useOpenApp } from './apps/open-app.hook.js'
|
|
15
3
|
export { AppHost } from './apps/app-host.js'
|
|
16
4
|
export type { AppHostFallbackState, AppHostProps } from './apps/app-host.js'
|
|
17
5
|
export { AppLogo } from './apps/app-logo.js'
|
|
18
6
|
export type { AppLogoProps } from './apps/app-logo.js'
|
|
19
7
|
// The OS node/session types (from the shell package, single import surface).
|
|
20
|
-
export type {
|
|
8
|
+
export type { NodeTarget, ResolvedView, UserNode } from '@astrale-os/shell'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useUser
|
|
3
|
-
*
|
|
2
|
+
* useUser — self as a Shell.User descendant, preserving canonical graph property
|
|
3
|
+
* keys. The provider PRE-RESOLVES it at boot (`primeUser`),
|
|
4
4
|
* so `useUser` is synchronous below the provider (API §2). OS-plane absent → throws
|
|
5
5
|
* descriptively to the nearest boundary; a never-primed shell suspends on first use
|
|
6
6
|
* (§1.4). FLAG: not yet re-rendered on rename — becomes live via a memory shape.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The OS-plane user store (API §2/§12.2). Provider-boot priming (`primeUser`) seeds
|
|
3
3
|
* the per-Shell user cache so `useUser` is synchronous, and tracks OS-plane
|
|
4
|
-
* availability: a missing/rejecting `shell.user.me`
|
|
5
|
-
*
|
|
4
|
+
* availability: a missing/rejecting `shell.user.me` is retained in the User cache
|
|
5
|
+
* without failing boot. A never-primed shell falls back to
|
|
6
6
|
* a lazy cold read that SUSPENDS on first use (§1.4).
|
|
7
7
|
*/
|
|
8
8
|
import type { Shell, UserNode } from '@astrale-os/shell'
|
|
@@ -16,14 +16,6 @@ export type UserEntry =
|
|
|
16
16
|
|
|
17
17
|
export const userCache = new WeakMap<Shell, UserEntry>()
|
|
18
18
|
|
|
19
|
-
/** Shells whose OS plane could not be resolved at boot — OS hooks read idle. */
|
|
20
|
-
const osPlaneDown = new WeakSet<Shell>()
|
|
21
|
-
|
|
22
|
-
/** Is the OS plane (shell.user/spaces/apps) usable for this shell? */
|
|
23
|
-
export function isOsPlaneAvailable(shell: Shell): boolean {
|
|
24
|
-
return !osPlaneDown.has(shell)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
interface UserSurface {
|
|
28
20
|
user?: { me?: () => Promise<UserNode> }
|
|
29
21
|
}
|
|
@@ -31,7 +23,7 @@ interface UserSurface {
|
|
|
31
23
|
function osPlaneError(cause: unknown): Error {
|
|
32
24
|
const detail = cause instanceof Error ? cause.message : String(cause)
|
|
33
25
|
return new Error(
|
|
34
|
-
`The
|
|
26
|
+
`The Shell User is unavailable (useUser): ` +
|
|
35
27
|
`shell.user.me() could not resolve ` +
|
|
36
28
|
`— the workspace/shell domain may not be installed for this identity. (${detail})`,
|
|
37
29
|
)
|
|
@@ -45,7 +37,6 @@ function osPlaneError(cause: unknown): Error {
|
|
|
45
37
|
export async function primeUser(shell: Shell): Promise<void> {
|
|
46
38
|
const me = (shell as unknown as UserSurface).user?.me
|
|
47
39
|
if (typeof me !== 'function') {
|
|
48
|
-
osPlaneDown.add(shell)
|
|
49
40
|
userCache.set(shell, {
|
|
50
41
|
status: 'unavailable',
|
|
51
42
|
error: osPlaneError('shell.user.me is not a function'),
|
|
@@ -54,10 +45,8 @@ export async function primeUser(shell: Shell): Promise<void> {
|
|
|
54
45
|
}
|
|
55
46
|
try {
|
|
56
47
|
const value = await me.call((shell as unknown as UserSurface).user)
|
|
57
|
-
osPlaneDown.delete(shell)
|
|
58
48
|
userCache.set(shell, { status: 'resolved', value })
|
|
59
49
|
} catch (error) {
|
|
60
|
-
osPlaneDown.add(shell)
|
|
61
50
|
userCache.set(shell, { status: 'unavailable', error: osPlaneError(error) })
|
|
62
51
|
}
|
|
63
52
|
}
|
package/src/index.ts
CHANGED
|
@@ -175,25 +175,8 @@ export {
|
|
|
175
175
|
export type { AstraleProps, ViewErrorBoundaryProps } from './components/index.js'
|
|
176
176
|
|
|
177
177
|
// Shell application conveniences.
|
|
178
|
-
export {
|
|
179
|
-
|
|
180
|
-
AppLogo,
|
|
181
|
-
useApps,
|
|
182
|
-
useCurrentSpace,
|
|
183
|
-
useOpenApp,
|
|
184
|
-
useSpace,
|
|
185
|
-
useSpaces,
|
|
186
|
-
useUser,
|
|
187
|
-
} from './application/index.js'
|
|
188
|
-
export type {
|
|
189
|
-
AppHostFallbackState,
|
|
190
|
-
AppHostProps,
|
|
191
|
-
AppLogoProps,
|
|
192
|
-
AppsResult,
|
|
193
|
-
CurrentSpaceResult,
|
|
194
|
-
SpaceResult,
|
|
195
|
-
SpacesResult,
|
|
196
|
-
} from './application/index.js'
|
|
178
|
+
export { AppHost, AppLogo, useOpenApp, useUser } from './application/index.js'
|
|
179
|
+
export type { AppHostFallbackState, AppHostProps, AppLogoProps } from './application/index.js'
|
|
197
180
|
|
|
198
181
|
// Canonical identity carriers used by View props and rich hooks.
|
|
199
182
|
export { NodeId } from '@astrale-os/sdk/graph/node'
|
|
@@ -205,7 +188,6 @@ export type { DomainBinding } from '@astrale-os/shell'
|
|
|
205
188
|
|
|
206
189
|
// Shell contracts retained at the package root for custom full-app composition.
|
|
207
190
|
export type {
|
|
208
|
-
App,
|
|
209
191
|
CloseResult,
|
|
210
192
|
CredentialSnapshot,
|
|
211
193
|
HostCapabilities,
|
|
@@ -215,7 +197,6 @@ export type {
|
|
|
215
197
|
NodeTarget,
|
|
216
198
|
Shell,
|
|
217
199
|
ShellConfig,
|
|
218
|
-
SpaceNode,
|
|
219
200
|
UserNode,
|
|
220
201
|
ViewLocation,
|
|
221
202
|
ViewPresentation,
|
|
@@ -5,7 +5,7 @@ import type { Domain } from '@astrale-os/sdk/schema'
|
|
|
5
5
|
import type {
|
|
6
6
|
CallableInputOf,
|
|
7
7
|
CallableResultOf,
|
|
8
|
-
|
|
8
|
+
ClassKey,
|
|
9
9
|
ResolvedFunction,
|
|
10
10
|
ResolvedMethod,
|
|
11
11
|
} from '@astrale-os/sdk/schema'
|
|
@@ -22,14 +22,14 @@ import { useAction as useAsyncAction } from '../graph/action.js'
|
|
|
22
22
|
import { useGraphStore } from '../graph/store.js'
|
|
23
23
|
import { bindingFor } from './domain.hook.js'
|
|
24
24
|
|
|
25
|
-
type
|
|
26
|
-
type
|
|
27
|
-
type StaticMethod = ResolvedMethod<unknown, unknown, true, ExecutableInheritance>
|
|
25
|
+
type InstanceMethod = ResolvedMethod<unknown, unknown, false, false>
|
|
26
|
+
type StaticMethod = ResolvedMethod<unknown, unknown, true, false>
|
|
28
27
|
type Callable = ResolvedFunction | InstanceMethod | StaticMethod
|
|
29
28
|
|
|
30
|
-
/**
|
|
29
|
+
/** Receiver identity with optional observed Class for early rejection; the Kernel checks live Class. */
|
|
31
30
|
export interface ActionReceiver {
|
|
32
31
|
readonly id: NodeId
|
|
32
|
+
readonly class?: ClassKey
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const QUERY_REFRESH = Symbol('shell-react.query-refresh')
|
|
@@ -114,7 +114,7 @@ export function useAction<C extends Callable>(
|
|
|
114
114
|
reference(
|
|
115
115
|
bindingRef.current.domain,
|
|
116
116
|
current,
|
|
117
|
-
)(receiverPath(args[0] as ActionReceiver)),
|
|
117
|
+
)(receiverPath(args[0] as ActionReceiver, current)),
|
|
118
118
|
args[1] as never,
|
|
119
119
|
request,
|
|
120
120
|
)
|
|
@@ -165,7 +165,11 @@ function isInstanceMethod(callable: Callable): callable is InstanceMethod {
|
|
|
165
165
|
return 'owner' in callable && callable.static === false
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
function receiverPath(receiver: ActionReceiver): Path {
|
|
168
|
+
function receiverPath(receiver: ActionReceiver, method: InstanceMethod): Path {
|
|
169
|
+
const observedClass = receiver.class
|
|
170
|
+
if (observedClass !== undefined && observedClass !== method.owner.key) {
|
|
171
|
+
throw new TypeError(`Method ${method.key} requires exact receiver Class ${method.owner.key}.`)
|
|
172
|
+
}
|
|
169
173
|
return Path.id(receiver.id)
|
|
170
174
|
}
|
|
171
175
|
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { App } from '@astrale-os/shell';
|
|
2
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js';
|
|
3
|
-
export interface AppsResult extends ReadState {
|
|
4
|
-
readonly apps: readonly App[];
|
|
5
|
-
}
|
|
6
|
-
/** Read the installed Domain applications available to the current Shell session. */
|
|
7
|
-
export declare function useApps(): AppsResult;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react';
|
|
2
|
-
import { useAsyncResource } from '../../graph/async.js';
|
|
3
|
-
import { useShell } from '../../session/shell.hook.js';
|
|
4
|
-
import { isOsPlaneAvailable } from '../user/user.store.js';
|
|
5
|
-
const NO_APPS = [];
|
|
6
|
-
/** Read the installed Domain applications available to the current Shell session. */
|
|
7
|
-
export function useApps() {
|
|
8
|
-
const shell = useShell();
|
|
9
|
-
const available = isOsPlaneAvailable(shell);
|
|
10
|
-
const result = useAsyncResource(available ? () => shell.apps.list() : null, NO_APPS, [shell, available]);
|
|
11
|
-
return useMemo(() => ({
|
|
12
|
-
apps: result.data,
|
|
13
|
-
pending: result.pending,
|
|
14
|
-
error: result.error,
|
|
15
|
-
live: false,
|
|
16
|
-
seq: 0,
|
|
17
|
-
refetch: result.refetch,
|
|
18
|
-
}), [result.data, result.pending, result.error, result.refetch]);
|
|
19
|
-
}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { SpaceNode } from '@astrale-os/shell';
|
|
2
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js';
|
|
3
|
-
export interface CurrentSpaceResult extends ReadState {
|
|
4
|
-
readonly space: SpaceNode | null;
|
|
5
|
-
}
|
|
6
|
-
export declare function useCurrentSpace(): CurrentSpaceResult;
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useCurrentSpace (API §12.2) — the selected space, projected from the shared
|
|
3
|
-
* per-Shell current-space store (so a switch anywhere re-themes it). One-shot
|
|
4
|
-
* today (§12 flag); OS-plane-unavailable reads idle.
|
|
5
|
-
*/
|
|
6
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
7
|
-
import { useShell } from '../../session/shell.hook.js';
|
|
8
|
-
import { isOsPlaneAvailable } from '../user/user.store.js';
|
|
9
|
-
import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store.js';
|
|
10
|
-
export function useCurrentSpace() {
|
|
11
|
-
const shell = useShell();
|
|
12
|
-
const available = isOsPlaneAvailable(shell);
|
|
13
|
-
const store = currentSpaceStore(shell);
|
|
14
|
-
useEffect(() => {
|
|
15
|
-
if (available)
|
|
16
|
-
store.ensure();
|
|
17
|
-
}, [store, available]);
|
|
18
|
-
const snap = useSyncExternalStore(available ? store.subscribe : noopSubscribe, available ? store.getSnapshot : idleSnapshot, available ? store.getSnapshot : idleSnapshot);
|
|
19
|
-
return useMemo(() => ({
|
|
20
|
-
space: snap.space,
|
|
21
|
-
pending: snap.pending,
|
|
22
|
-
error: snap.error,
|
|
23
|
-
live: false,
|
|
24
|
-
seq: 0,
|
|
25
|
-
refetch: store.refresh,
|
|
26
|
-
}), [snap, store]);
|
|
27
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared current-space store (API §12.2). ONE reactive resolver per Shell, so
|
|
3
|
-
* useCurrentSpace / useApps / useSpaces.selected all observe the SAME selected
|
|
4
|
-
* space: a `switch` in any window re-themes every consumer in this window at once,
|
|
5
|
-
* with a manual optimistic override applied ahead of the round-trip (§1.6). This
|
|
6
|
-
* closes the same-window cross-hook divergence; cross-WINDOW liveness still waits
|
|
7
|
-
* on the memory-shape move (§12 flag).
|
|
8
|
-
*/
|
|
9
|
-
import type { Shell, SpaceNode } from '@astrale-os/shell';
|
|
10
|
-
export interface CurrentSpaceSnapshot {
|
|
11
|
-
readonly space: SpaceNode | null;
|
|
12
|
-
readonly pending: boolean;
|
|
13
|
-
readonly error: unknown;
|
|
14
|
-
}
|
|
15
|
-
export declare const IDLE_SNAPSHOT: CurrentSpaceSnapshot;
|
|
16
|
-
export declare const idleSnapshot: () => CurrentSpaceSnapshot;
|
|
17
|
-
export declare const noopSubscribe: () => (() => void);
|
|
18
|
-
export interface CurrentSpaceStore {
|
|
19
|
-
subscribe(cb: () => void): () => void;
|
|
20
|
-
getSnapshot(): CurrentSpaceSnapshot;
|
|
21
|
-
/** Kick a first load if the confirmed value was never fetched. */
|
|
22
|
-
ensure(): void;
|
|
23
|
-
/** Refetch the confirmed selected space; resolves when it lands. */
|
|
24
|
-
refresh(): Promise<void>;
|
|
25
|
-
/** Apply an optimistic override (a switch in flight) — every consumer re-renders. */
|
|
26
|
-
setOptimistic(space: SpaceNode | null): void;
|
|
27
|
-
/** Drop the optimistic override, revealing confirmed truth again. */
|
|
28
|
-
clearOptimistic(): void;
|
|
29
|
-
}
|
|
30
|
-
/** The one current-space store for a Shell (created on first touch). */
|
|
31
|
-
export declare function currentSpaceStore(shell: Shell): CurrentSpaceStore;
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// Stable idles for when the OS plane is unavailable (§12.2) — a fixed snapshot
|
|
2
|
-
// and a no-op subscription keep `useSyncExternalStore` tear-free without touching
|
|
3
|
-
// (and re-triggering a fetch on) the real current-space store.
|
|
4
|
-
export const IDLE_SNAPSHOT = { space: null, pending: false, error: undefined };
|
|
5
|
-
export const idleSnapshot = () => IDLE_SNAPSHOT;
|
|
6
|
-
export const noopSubscribe = () => () => { };
|
|
7
|
-
function createStore(load) {
|
|
8
|
-
let confirmed = null;
|
|
9
|
-
let loaded = false;
|
|
10
|
-
let pending = false;
|
|
11
|
-
let error = undefined;
|
|
12
|
-
let override = null;
|
|
13
|
-
let seq = 0;
|
|
14
|
-
let snap = { space: null, pending: false, error: undefined };
|
|
15
|
-
const listeners = new Set();
|
|
16
|
-
/** Recompute the projection (override wins) and notify only on a real change. */
|
|
17
|
-
function rebuild() {
|
|
18
|
-
const space = override !== null ? override.space : confirmed;
|
|
19
|
-
if (space === snap.space && pending === snap.pending && error === snap.error)
|
|
20
|
-
return;
|
|
21
|
-
snap = { space, pending, error };
|
|
22
|
-
for (const l of listeners)
|
|
23
|
-
l();
|
|
24
|
-
}
|
|
25
|
-
function fetch() {
|
|
26
|
-
const my = ++seq;
|
|
27
|
-
pending = true;
|
|
28
|
-
rebuild();
|
|
29
|
-
return load().then((space) => {
|
|
30
|
-
if (my !== seq)
|
|
31
|
-
return;
|
|
32
|
-
confirmed = space;
|
|
33
|
-
loaded = true;
|
|
34
|
-
pending = false;
|
|
35
|
-
error = undefined;
|
|
36
|
-
rebuild();
|
|
37
|
-
}, (e) => {
|
|
38
|
-
if (my !== seq)
|
|
39
|
-
return;
|
|
40
|
-
pending = false;
|
|
41
|
-
error = e;
|
|
42
|
-
rebuild();
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return {
|
|
46
|
-
subscribe(cb) {
|
|
47
|
-
listeners.add(cb);
|
|
48
|
-
if (!loaded && !pending)
|
|
49
|
-
void fetch();
|
|
50
|
-
return () => listeners.delete(cb);
|
|
51
|
-
},
|
|
52
|
-
getSnapshot: () => snap,
|
|
53
|
-
ensure() {
|
|
54
|
-
if (!loaded && !pending)
|
|
55
|
-
void fetch();
|
|
56
|
-
},
|
|
57
|
-
refresh: () => fetch(),
|
|
58
|
-
setOptimistic(space) {
|
|
59
|
-
override = { space };
|
|
60
|
-
rebuild();
|
|
61
|
-
},
|
|
62
|
-
clearOptimistic() {
|
|
63
|
-
if (override === null)
|
|
64
|
-
return;
|
|
65
|
-
override = null;
|
|
66
|
-
rebuild();
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
const stores = new WeakMap();
|
|
71
|
-
/** The one current-space store for a Shell (created on first touch). */
|
|
72
|
-
export function currentSpaceStore(shell) {
|
|
73
|
-
let s = stores.get(shell);
|
|
74
|
-
if (s === undefined) {
|
|
75
|
-
s = createStore(() => shell.user.currentSpace());
|
|
76
|
-
stores.set(shell, s);
|
|
77
|
-
}
|
|
78
|
-
return s;
|
|
79
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { Path } from '@astrale-os/sdk/graph/path';
|
|
2
|
-
/**
|
|
3
|
-
* useSpace(ref) (API §12.2) — a single space by ref; `null`/`undefined` (or an
|
|
4
|
-
* unavailable OS plane) is idle (§1.3). One-shot today (§12 flag).
|
|
5
|
-
*/
|
|
6
|
-
import { useMemo } from 'react';
|
|
7
|
-
import { useAsyncResource } from '../../graph/async.js';
|
|
8
|
-
import { isIdleInput } from '../../graph/memory/read/idle.js';
|
|
9
|
-
import { useShell } from '../../session/shell.hook.js';
|
|
10
|
-
import { isOsPlaneAvailable } from '../user/user.store.js';
|
|
11
|
-
export function useSpace(ref) {
|
|
12
|
-
const shell = useShell();
|
|
13
|
-
const idle = isIdleInput(ref) || !isOsPlaneAvailable(shell);
|
|
14
|
-
// The inline guard (not the stored `idle`) narrows `ref` to non-null below.
|
|
15
|
-
const key = idle ? null : targetKey(ref);
|
|
16
|
-
const res = useAsyncResource(idle ? null : () => shell.spaces.get(ref), null, [shell, key]);
|
|
17
|
-
return useMemo(() => ({
|
|
18
|
-
space: idle ? null : res.data,
|
|
19
|
-
pending: idle ? false : res.pending,
|
|
20
|
-
error: idle ? undefined : res.error,
|
|
21
|
-
live: false,
|
|
22
|
-
seq: 0,
|
|
23
|
-
refetch: res.refetch,
|
|
24
|
-
}), [idle, res]);
|
|
25
|
-
}
|
|
26
|
-
function targetKey(input) {
|
|
27
|
-
return typeof input === 'string' ? `text:${input}` : `path:${Path.from(input).raw}`;
|
|
28
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { NodeTarget, SpaceNode } from '@astrale-os/shell';
|
|
2
|
-
import type { Action } from '../../graph/action.js';
|
|
3
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js';
|
|
4
|
-
export interface SpacesResult extends ReadState {
|
|
5
|
-
/** Spaces-dir children, live (one-shot today), the caller's readable set. */
|
|
6
|
-
readonly spaces: readonly SpaceNode[];
|
|
7
|
-
/** The `selected_space` target — optimistically ahead during a `switch`. */
|
|
8
|
-
readonly selected: SpaceNode | null;
|
|
9
|
-
/** `user::switchSpace` with a manual optimistic overlay (§1.6). */
|
|
10
|
-
readonly switch: Action<[NodeTarget], void>;
|
|
11
|
-
}
|
|
12
|
-
export declare function useSpaces(): SpacesResult;
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Path } from '@astrale-os/sdk/graph/path';
|
|
2
|
-
/**
|
|
3
|
-
* useSpaces (API §12.2) — the spaces list, the selected space, and a `switch`
|
|
4
|
-
* Action that repoints the `selected_space` edge. Reads the SAME per-Shell
|
|
5
|
-
* current-space store as useCurrentSpace/useApps, so a switch re-themes every
|
|
6
|
-
* consumer in the window at once — optimistically ahead of the round-trip (§1.6),
|
|
7
|
-
* commit reconciles, failure rolls back. Reads are one-shot today (§12 flag).
|
|
8
|
-
*/
|
|
9
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react';
|
|
10
|
-
import { useAction } from '../../graph/action.js';
|
|
11
|
-
import { useAsyncResource } from '../../graph/async.js';
|
|
12
|
-
import { useShell } from '../../session/shell.hook.js';
|
|
13
|
-
import { isOsPlaneAvailable } from '../user/user.store.js';
|
|
14
|
-
import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store.js';
|
|
15
|
-
const NO_SPACES = [];
|
|
16
|
-
export function useSpaces() {
|
|
17
|
-
const shell = useShell();
|
|
18
|
-
const available = isOsPlaneAvailable(shell);
|
|
19
|
-
const store = currentSpaceStore(shell);
|
|
20
|
-
const spacesRes = useAsyncResource(available ? () => shell.spaces.list() : null, NO_SPACES, [shell, available]);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (available)
|
|
23
|
-
store.ensure();
|
|
24
|
-
}, [store, available]);
|
|
25
|
-
const selectedSnap = useSyncExternalStore(available ? store.subscribe : noopSubscribe, available ? store.getSnapshot : idleSnapshot, available ? store.getSnapshot : idleSnapshot);
|
|
26
|
-
const spaces = spacesRes.data;
|
|
27
|
-
const switchAction = useAction(async (ref) => {
|
|
28
|
-
// Optimistic override on the SHARED store, so useCurrentSpace / useApps ahead
|
|
29
|
-
// of the round-trip too (§12.2). Only overlay a known target (a path ref that
|
|
30
|
-
// matches nothing in the list simply skips the preview).
|
|
31
|
-
const target = findSpace(spaces, ref);
|
|
32
|
-
if (target !== null)
|
|
33
|
-
store.setOptimistic(target);
|
|
34
|
-
try {
|
|
35
|
-
await shell.user.switchSpace(ref);
|
|
36
|
-
await store.refresh(); // reconcile confirmed truth BEFORE dropping the preview
|
|
37
|
-
store.clearOptimistic();
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
store.clearOptimistic(); // rollback: reveal the confirmed (previous) space
|
|
41
|
-
throw error;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
return useMemo(() => ({
|
|
45
|
-
spaces,
|
|
46
|
-
selected: selectedSnap.space,
|
|
47
|
-
switch: switchAction,
|
|
48
|
-
pending: spacesRes.pending || selectedSnap.pending,
|
|
49
|
-
error: spacesRes.error ?? selectedSnap.error,
|
|
50
|
-
live: false,
|
|
51
|
-
seq: 0,
|
|
52
|
-
refetch: async () => {
|
|
53
|
-
await Promise.all([spacesRes.refetch(), store.refresh()]);
|
|
54
|
-
},
|
|
55
|
-
}), [
|
|
56
|
-
spaces,
|
|
57
|
-
selectedSnap,
|
|
58
|
-
switchAction,
|
|
59
|
-
spacesRes.pending,
|
|
60
|
-
spacesRes.error,
|
|
61
|
-
spacesRes.refetch,
|
|
62
|
-
store,
|
|
63
|
-
]);
|
|
64
|
-
}
|
|
65
|
-
/** The space in `spaces` denoted by `ref`, or `null` (path refs don't match by id). */
|
|
66
|
-
function findSpace(spaces, ref) {
|
|
67
|
-
const id = refIdOf(ref);
|
|
68
|
-
if (id === null)
|
|
69
|
-
return null;
|
|
70
|
-
return spaces.find((s) => s.id === id) ?? null;
|
|
71
|
-
}
|
|
72
|
-
/** A bound-node-ish subject carries a graph id. */
|
|
73
|
-
function hasId(x) {
|
|
74
|
-
return typeof x === 'object' && x !== null && 'id' in x;
|
|
75
|
-
}
|
|
76
|
-
/** The graph id a subject denotes, or `null` when it is a path (no id to match). */
|
|
77
|
-
function refIdOf(x) {
|
|
78
|
-
if (hasId(x))
|
|
79
|
-
return x.id;
|
|
80
|
-
if (typeof x === 'string' && !x.startsWith('@') && !x.startsWith('/'))
|
|
81
|
-
return x;
|
|
82
|
-
try {
|
|
83
|
-
const path = Path.from(x);
|
|
84
|
-
return path.ast.anchor.kind === 'id' && path.ast.steps.length === 0 ? path.ast.anchor.id : null;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { App } from '@astrale-os/shell'
|
|
2
|
-
|
|
3
|
-
import { useMemo } from 'react'
|
|
4
|
-
|
|
5
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js'
|
|
6
|
-
|
|
7
|
-
import { useAsyncResource } from '../../graph/async.js'
|
|
8
|
-
import { useShell } from '../../session/shell.hook.js'
|
|
9
|
-
import { isOsPlaneAvailable } from '../user/user.store.js'
|
|
10
|
-
|
|
11
|
-
const NO_APPS: readonly App[] = []
|
|
12
|
-
|
|
13
|
-
export interface AppsResult extends ReadState {
|
|
14
|
-
readonly apps: readonly App[]
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** Read the installed Domain applications available to the current Shell session. */
|
|
18
|
-
export function useApps(): AppsResult {
|
|
19
|
-
const shell = useShell()
|
|
20
|
-
const available = isOsPlaneAvailable(shell)
|
|
21
|
-
const result = useAsyncResource<readonly App[]>(
|
|
22
|
-
available ? () => shell.apps.list() : null,
|
|
23
|
-
NO_APPS,
|
|
24
|
-
[shell, available],
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
return useMemo<AppsResult>(
|
|
28
|
-
() => ({
|
|
29
|
-
apps: result.data,
|
|
30
|
-
pending: result.pending,
|
|
31
|
-
error: result.error,
|
|
32
|
-
live: false,
|
|
33
|
-
seq: 0,
|
|
34
|
-
refetch: result.refetch,
|
|
35
|
-
}),
|
|
36
|
-
[result.data, result.pending, result.error, result.refetch],
|
|
37
|
-
)
|
|
38
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { SpaceNode } from '@astrale-os/shell'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useCurrentSpace (API §12.2) — the selected space, projected from the shared
|
|
5
|
-
* per-Shell current-space store (so a switch anywhere re-themes it). One-shot
|
|
6
|
-
* today (§12 flag); OS-plane-unavailable reads idle.
|
|
7
|
-
*/
|
|
8
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react'
|
|
9
|
-
|
|
10
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js'
|
|
11
|
-
|
|
12
|
-
import { useShell } from '../../session/shell.hook.js'
|
|
13
|
-
import { isOsPlaneAvailable } from '../user/user.store.js'
|
|
14
|
-
import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store.js'
|
|
15
|
-
|
|
16
|
-
export interface CurrentSpaceResult extends ReadState {
|
|
17
|
-
readonly space: SpaceNode | null
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function useCurrentSpace(): CurrentSpaceResult {
|
|
21
|
-
const shell = useShell()
|
|
22
|
-
const available = isOsPlaneAvailable(shell)
|
|
23
|
-
const store = currentSpaceStore(shell)
|
|
24
|
-
useEffect(() => {
|
|
25
|
-
if (available) store.ensure()
|
|
26
|
-
}, [store, available])
|
|
27
|
-
const snap = useSyncExternalStore(
|
|
28
|
-
available ? store.subscribe : noopSubscribe,
|
|
29
|
-
available ? store.getSnapshot : idleSnapshot,
|
|
30
|
-
available ? store.getSnapshot : idleSnapshot,
|
|
31
|
-
)
|
|
32
|
-
return useMemo(
|
|
33
|
-
() => ({
|
|
34
|
-
space: snap.space,
|
|
35
|
-
pending: snap.pending,
|
|
36
|
-
error: snap.error,
|
|
37
|
-
live: false,
|
|
38
|
-
seq: 0,
|
|
39
|
-
refetch: store.refresh,
|
|
40
|
-
}),
|
|
41
|
-
[snap, store],
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The shared current-space store (API §12.2). ONE reactive resolver per Shell, so
|
|
3
|
-
* useCurrentSpace / useApps / useSpaces.selected all observe the SAME selected
|
|
4
|
-
* space: a `switch` in any window re-themes every consumer in this window at once,
|
|
5
|
-
* with a manual optimistic override applied ahead of the round-trip (§1.6). This
|
|
6
|
-
* closes the same-window cross-hook divergence; cross-WINDOW liveness still waits
|
|
7
|
-
* on the memory-shape move (§12 flag).
|
|
8
|
-
*/
|
|
9
|
-
import type { Shell, SpaceNode } from '@astrale-os/shell'
|
|
10
|
-
|
|
11
|
-
export interface CurrentSpaceSnapshot {
|
|
12
|
-
readonly space: SpaceNode | null
|
|
13
|
-
readonly pending: boolean
|
|
14
|
-
readonly error: unknown
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Stable idles for when the OS plane is unavailable (§12.2) — a fixed snapshot
|
|
18
|
-
// and a no-op subscription keep `useSyncExternalStore` tear-free without touching
|
|
19
|
-
// (and re-triggering a fetch on) the real current-space store.
|
|
20
|
-
export const IDLE_SNAPSHOT: CurrentSpaceSnapshot = { space: null, pending: false, error: undefined }
|
|
21
|
-
export const idleSnapshot = (): CurrentSpaceSnapshot => IDLE_SNAPSHOT
|
|
22
|
-
export const noopSubscribe = (): (() => void) => () => {}
|
|
23
|
-
|
|
24
|
-
export interface CurrentSpaceStore {
|
|
25
|
-
subscribe(cb: () => void): () => void
|
|
26
|
-
getSnapshot(): CurrentSpaceSnapshot
|
|
27
|
-
/** Kick a first load if the confirmed value was never fetched. */
|
|
28
|
-
ensure(): void
|
|
29
|
-
/** Refetch the confirmed selected space; resolves when it lands. */
|
|
30
|
-
refresh(): Promise<void>
|
|
31
|
-
/** Apply an optimistic override (a switch in flight) — every consumer re-renders. */
|
|
32
|
-
setOptimistic(space: SpaceNode | null): void
|
|
33
|
-
/** Drop the optimistic override, revealing confirmed truth again. */
|
|
34
|
-
clearOptimistic(): void
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function createStore(load: () => Promise<SpaceNode | null>): CurrentSpaceStore {
|
|
38
|
-
let confirmed: SpaceNode | null = null
|
|
39
|
-
let loaded = false
|
|
40
|
-
let pending = false
|
|
41
|
-
let error: unknown = undefined
|
|
42
|
-
let override: { space: SpaceNode | null } | null = null
|
|
43
|
-
let seq = 0
|
|
44
|
-
let snap: CurrentSpaceSnapshot = { space: null, pending: false, error: undefined }
|
|
45
|
-
const listeners = new Set<() => void>()
|
|
46
|
-
|
|
47
|
-
/** Recompute the projection (override wins) and notify only on a real change. */
|
|
48
|
-
function rebuild(): void {
|
|
49
|
-
const space = override !== null ? override.space : confirmed
|
|
50
|
-
if (space === snap.space && pending === snap.pending && error === snap.error) return
|
|
51
|
-
snap = { space, pending, error }
|
|
52
|
-
for (const l of listeners) l()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function fetch(): Promise<void> {
|
|
56
|
-
const my = ++seq
|
|
57
|
-
pending = true
|
|
58
|
-
rebuild()
|
|
59
|
-
return load().then(
|
|
60
|
-
(space) => {
|
|
61
|
-
if (my !== seq) return
|
|
62
|
-
confirmed = space
|
|
63
|
-
loaded = true
|
|
64
|
-
pending = false
|
|
65
|
-
error = undefined
|
|
66
|
-
rebuild()
|
|
67
|
-
},
|
|
68
|
-
(e: unknown) => {
|
|
69
|
-
if (my !== seq) return
|
|
70
|
-
pending = false
|
|
71
|
-
error = e
|
|
72
|
-
rebuild()
|
|
73
|
-
},
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
subscribe(cb) {
|
|
79
|
-
listeners.add(cb)
|
|
80
|
-
if (!loaded && !pending) void fetch()
|
|
81
|
-
return () => listeners.delete(cb)
|
|
82
|
-
},
|
|
83
|
-
getSnapshot: () => snap,
|
|
84
|
-
ensure() {
|
|
85
|
-
if (!loaded && !pending) void fetch()
|
|
86
|
-
},
|
|
87
|
-
refresh: () => fetch(),
|
|
88
|
-
setOptimistic(space) {
|
|
89
|
-
override = { space }
|
|
90
|
-
rebuild()
|
|
91
|
-
},
|
|
92
|
-
clearOptimistic() {
|
|
93
|
-
if (override === null) return
|
|
94
|
-
override = null
|
|
95
|
-
rebuild()
|
|
96
|
-
},
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const stores = new WeakMap<Shell, CurrentSpaceStore>()
|
|
101
|
-
|
|
102
|
-
/** The one current-space store for a Shell (created on first touch). */
|
|
103
|
-
export function currentSpaceStore(shell: Shell): CurrentSpaceStore {
|
|
104
|
-
let s = stores.get(shell)
|
|
105
|
-
if (s === undefined) {
|
|
106
|
-
s = createStore(() => shell.user.currentSpace())
|
|
107
|
-
stores.set(shell, s)
|
|
108
|
-
}
|
|
109
|
-
return s
|
|
110
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { PathLike } from '@astrale-os/sdk/graph/path'
|
|
2
|
-
import type { NodeTarget, SpaceNode } from '@astrale-os/shell'
|
|
3
|
-
|
|
4
|
-
import { Path } from '@astrale-os/sdk/graph/path'
|
|
5
|
-
/**
|
|
6
|
-
* useSpace(ref) (API §12.2) — a single space by ref; `null`/`undefined` (or an
|
|
7
|
-
* unavailable OS plane) is idle (§1.3). One-shot today (§12 flag).
|
|
8
|
-
*/
|
|
9
|
-
import { useMemo } from 'react'
|
|
10
|
-
|
|
11
|
-
import type { CurrentSpaceResult } from './current-space.hook.js'
|
|
12
|
-
|
|
13
|
-
import { useAsyncResource } from '../../graph/async.js'
|
|
14
|
-
import { isIdleInput } from '../../graph/memory/read/idle.js'
|
|
15
|
-
import { useShell } from '../../session/shell.hook.js'
|
|
16
|
-
import { isOsPlaneAvailable } from '../user/user.store.js'
|
|
17
|
-
|
|
18
|
-
export type SpaceResult = CurrentSpaceResult
|
|
19
|
-
|
|
20
|
-
export function useSpace(ref: NodeTarget | null | undefined): SpaceResult {
|
|
21
|
-
const shell = useShell()
|
|
22
|
-
const idle = isIdleInput(ref) || !isOsPlaneAvailable(shell)
|
|
23
|
-
// The inline guard (not the stored `idle`) narrows `ref` to non-null below.
|
|
24
|
-
const key = idle ? null : targetKey(ref as NodeTarget)
|
|
25
|
-
const res = useAsyncResource<SpaceNode | null>(
|
|
26
|
-
idle ? null : () => shell.spaces.get(ref as NodeTarget),
|
|
27
|
-
null,
|
|
28
|
-
[shell, key],
|
|
29
|
-
)
|
|
30
|
-
return useMemo(
|
|
31
|
-
() => ({
|
|
32
|
-
space: idle ? null : res.data,
|
|
33
|
-
pending: idle ? false : res.pending,
|
|
34
|
-
error: idle ? undefined : res.error,
|
|
35
|
-
live: false,
|
|
36
|
-
seq: 0,
|
|
37
|
-
refetch: res.refetch,
|
|
38
|
-
}),
|
|
39
|
-
[idle, res],
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function targetKey(input: NodeTarget): string {
|
|
44
|
-
return typeof input === 'string' ? `text:${input}` : `path:${Path.from(input as PathLike).raw}`
|
|
45
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import type { PathLike } from '@astrale-os/sdk/graph/path'
|
|
2
|
-
import type { NodeTarget, SpaceNode } from '@astrale-os/shell'
|
|
3
|
-
|
|
4
|
-
import { Path } from '@astrale-os/sdk/graph/path'
|
|
5
|
-
/**
|
|
6
|
-
* useSpaces (API §12.2) — the spaces list, the selected space, and a `switch`
|
|
7
|
-
* Action that repoints the `selected_space` edge. Reads the SAME per-Shell
|
|
8
|
-
* current-space store as useCurrentSpace/useApps, so a switch re-themes every
|
|
9
|
-
* consumer in the window at once — optimistically ahead of the round-trip (§1.6),
|
|
10
|
-
* commit reconciles, failure rolls back. Reads are one-shot today (§12 flag).
|
|
11
|
-
*/
|
|
12
|
-
import { useEffect, useMemo, useSyncExternalStore } from 'react'
|
|
13
|
-
|
|
14
|
-
import type { Action } from '../../graph/action.js'
|
|
15
|
-
import type { ReadState } from '../../graph/memory/read/envelope.js'
|
|
16
|
-
|
|
17
|
-
import { useAction } from '../../graph/action.js'
|
|
18
|
-
import { useAsyncResource } from '../../graph/async.js'
|
|
19
|
-
import { useShell } from '../../session/shell.hook.js'
|
|
20
|
-
import { isOsPlaneAvailable } from '../user/user.store.js'
|
|
21
|
-
import { currentSpaceStore, idleSnapshot, noopSubscribe } from './current-space.store.js'
|
|
22
|
-
|
|
23
|
-
const NO_SPACES: readonly SpaceNode[] = []
|
|
24
|
-
|
|
25
|
-
export interface SpacesResult extends ReadState {
|
|
26
|
-
/** Spaces-dir children, live (one-shot today), the caller's readable set. */
|
|
27
|
-
readonly spaces: readonly SpaceNode[]
|
|
28
|
-
/** The `selected_space` target — optimistically ahead during a `switch`. */
|
|
29
|
-
readonly selected: SpaceNode | null
|
|
30
|
-
/** `user::switchSpace` with a manual optimistic overlay (§1.6). */
|
|
31
|
-
readonly switch: Action<[NodeTarget], void>
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function useSpaces(): SpacesResult {
|
|
35
|
-
const shell = useShell()
|
|
36
|
-
const available = isOsPlaneAvailable(shell)
|
|
37
|
-
const store = currentSpaceStore(shell)
|
|
38
|
-
|
|
39
|
-
const spacesRes = useAsyncResource<readonly SpaceNode[]>(
|
|
40
|
-
available ? () => shell.spaces.list() : null,
|
|
41
|
-
NO_SPACES,
|
|
42
|
-
[shell, available],
|
|
43
|
-
)
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (available) store.ensure()
|
|
46
|
-
}, [store, available])
|
|
47
|
-
const selectedSnap = useSyncExternalStore(
|
|
48
|
-
available ? store.subscribe : noopSubscribe,
|
|
49
|
-
available ? store.getSnapshot : idleSnapshot,
|
|
50
|
-
available ? store.getSnapshot : idleSnapshot,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
const spaces = spacesRes.data
|
|
54
|
-
|
|
55
|
-
const switchAction = useAction(async (ref: NodeTarget): Promise<void> => {
|
|
56
|
-
// Optimistic override on the SHARED store, so useCurrentSpace / useApps ahead
|
|
57
|
-
// of the round-trip too (§12.2). Only overlay a known target (a path ref that
|
|
58
|
-
// matches nothing in the list simply skips the preview).
|
|
59
|
-
const target = findSpace(spaces, ref)
|
|
60
|
-
if (target !== null) store.setOptimistic(target)
|
|
61
|
-
try {
|
|
62
|
-
await shell.user.switchSpace(ref)
|
|
63
|
-
await store.refresh() // reconcile confirmed truth BEFORE dropping the preview
|
|
64
|
-
store.clearOptimistic()
|
|
65
|
-
} catch (error) {
|
|
66
|
-
store.clearOptimistic() // rollback: reveal the confirmed (previous) space
|
|
67
|
-
throw error
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
return useMemo<SpacesResult>(
|
|
72
|
-
() => ({
|
|
73
|
-
spaces,
|
|
74
|
-
selected: selectedSnap.space,
|
|
75
|
-
switch: switchAction,
|
|
76
|
-
pending: spacesRes.pending || selectedSnap.pending,
|
|
77
|
-
error: spacesRes.error ?? selectedSnap.error,
|
|
78
|
-
live: false,
|
|
79
|
-
seq: 0,
|
|
80
|
-
refetch: async () => {
|
|
81
|
-
await Promise.all([spacesRes.refetch(), store.refresh()])
|
|
82
|
-
},
|
|
83
|
-
}),
|
|
84
|
-
[
|
|
85
|
-
spaces,
|
|
86
|
-
selectedSnap,
|
|
87
|
-
switchAction,
|
|
88
|
-
spacesRes.pending,
|
|
89
|
-
spacesRes.error,
|
|
90
|
-
spacesRes.refetch,
|
|
91
|
-
store,
|
|
92
|
-
],
|
|
93
|
-
)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** The space in `spaces` denoted by `ref`, or `null` (path refs don't match by id). */
|
|
97
|
-
function findSpace(spaces: readonly SpaceNode[], ref: NodeTarget): SpaceNode | null {
|
|
98
|
-
const id = refIdOf(ref)
|
|
99
|
-
if (id === null) return null
|
|
100
|
-
return spaces.find((s) => s.id === id) ?? null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** A bound-node-ish subject carries a graph id. */
|
|
104
|
-
function hasId(x: unknown): x is { readonly id: string } {
|
|
105
|
-
return typeof x === 'object' && x !== null && 'id' in x
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The graph id a subject denotes, or `null` when it is a path (no id to match). */
|
|
109
|
-
function refIdOf(x: NodeTarget | { readonly id: string }): string | null {
|
|
110
|
-
if (hasId(x)) return x.id
|
|
111
|
-
if (typeof x === 'string' && !x.startsWith('@') && !x.startsWith('/')) return x
|
|
112
|
-
try {
|
|
113
|
-
const path = Path.from(x as PathLike)
|
|
114
|
-
return path.ast.anchor.kind === 'id' && path.ast.steps.length === 0 ? path.ast.anchor.id : null
|
|
115
|
-
} catch {
|
|
116
|
-
return null
|
|
117
|
-
}
|
|
118
|
-
}
|