@phreshos/react 0.1.16 → 0.1.17

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/README.md CHANGED
@@ -1,162 +1,91 @@
1
1
  # `@phreshos/react`
2
2
 
3
- The React SDK adapts `@phreshos/client` to React. It is not another domain
4
- authority and does not define Program, Process, Endpoint, Client, Server, or
5
- Window objects.
6
-
7
- ## Package status
8
-
9
- This package is one component of a larger architecture that is still under
10
- active testing. The architecture's components will be released in stages as
11
- their contracts and integrations are verified.
12
-
13
- `@phreshos/react` is not intended to be used on its own. It adapts the Client
14
- SDK to React and therefore requires both `@phreshos/client` and React as peer
15
- dependencies.
16
-
17
- It provides two kinds of adapter:
18
-
19
- - `ContextProvider` resolves exactly the runtime values named by its required
20
- `provide` prop, then exposes them synchronously through `useProgram()`,
21
- `useProcess()`, and `useParent()`.
22
- - `SystemProvider` subscribes before reading exactly the system values named by its
23
- required `provide` prop. `useSystemAppearance()`, `useDesktopPreferences()`,
24
- `useDesktopSize()`, and `usePointerPosition()` expose those values synchronously
25
- after resolution.
26
- - `useSubscribe()` owns named or all-event registrations for one mounted React
27
- consumer.
28
- - `useProgramState(program)`, `useProcessState(process)`,
29
- `useServiceState(service)`, and `useWindowState(window)` explicitly compose
30
- existing reads with future live events for one mounted consumer. They add no
31
- state operation to another SDK.
32
- - `useSubscribeAsks()` adapts an Endpoint traffic surface's question
33
- subscription, while `useSubscribeAnswers()` adapts a Server traffic
34
- surface's answer subscription.
35
-
36
- Window needs no React resolution: `context.window` is already a synchronous,
37
- silent capability object. `ContextProvider` therefore has no `window` selection
38
- and the React SDK exposes no pass-through `useWindow()` hook.
39
-
40
- The domain state hooks return only mutable state; identity and immutable
41
- metadata remain on the supplied handle:
42
-
43
- ```ts
44
- useProgramState(program)
45
- // { installed, processes } | undefined
46
-
47
- useProcessState(process)
48
- // { exited, serverExists, clientExists } | undefined
49
-
50
- useServiceState(service)
51
- // { exists } | undefined
52
-
53
- useWindowState(window)
54
- // WindowState | undefined
55
- ```
3
+ Runtime-neutral React adapters for PhreshOS contracts.
56
4
 
57
- `WindowState` contains the observable Window properties only. The command-only
58
- `window.surface` capability is intentionally absent: Program code may replace
59
- or remove its target but cannot read or subscribe to it.
60
-
61
- `undefined` means only that the initial explicit reads are pending. Each hook
62
- opens its live subscriptions before those reads, then applies any intervening
63
- events so an older result cannot overwrite newer state. If an initial read
64
- rejects, the hook throws that original value during render for the nearest
65
- React error boundary. It does not invent a fallback, retry, wait for a stopped
66
- Client, or use an old Window snapshot as the fallback for that rejection. State
67
- exists only while the hook is mounted and every registration is cleaned up on
68
- unmount.
69
-
70
- The Window capability outlives a stopped Client, but live Window state does
71
- not. A component that spans Client lifecycle should use `useProcessState()` to
72
- mount its `useWindowState()` child only while `clientExists` is true. Calling
73
- the Window hook while the Client is absent lets the existing Window reads
74
- reject normally.
75
-
76
- The ordinary-event and traffic hooks retain the latest value returned by their
77
- projector. A named `useSubscribe()` may omit that callback; its default
78
- projector is `message => message`, so the hook retains the latest message
79
- unchanged. The all-event form receives a correlated capture:
5
+ The React SDK receives explicit Core handles and live sources. It does not
6
+ initialize transport, import the Client SDK, access the browser, or define
7
+ domain state.
80
8
 
81
- ```tsx
82
- import { system } from "@phreshos/client"
83
- import { useSubscribe } from "@phreshos/react"
9
+ ## Installation
84
10
 
85
- const desktop = useSubscribe(system.desktop, "resize")
11
+ | Package manager | Command |
12
+ | --- | --- |
13
+ | npm | `npm install @phreshos/react` |
14
+ | pnpm | `pnpm add @phreshos/react` |
15
+ | Bun | `bun add @phreshos/react` |
16
+ | Yarn | `yarn add @phreshos/react` |
86
17
 
87
- const latest = useSubscribe(system.desktop, capture => {
88
- if (capture.event === "resize") return capture.message
89
- })
90
- ```
18
+ `@phreshos/core` and React are peer dependencies.
91
19
 
92
- System reads are asynchronous while subscriptions are live-only. `SystemProvider`
93
- subscribes before requesting each selected snapshot and prevents an older read
94
- from overwriting a newer event:
20
+ ## Providers
95
21
 
96
22
  ```tsx
97
- import { SystemProvider, useDesktopPreferences, useDesktopSize, useSystemAppearance } from "@phreshos/react"
23
+ import { ContextProvider, SystemProvider } from "@phreshos/react"
24
+
25
+ <SystemProvider
26
+ appearance={system.appearance}
27
+ desktopSurface={system.desktop.surface}
28
+ desktopPointer={system.desktop.pointer}
29
+ desktopPreferences={system.desktop.preferences}
30
+ >
31
+ <ContextProvider
32
+ program={() => context.program()}
33
+ process={() => context.process()}
34
+ parent={() => context.process().then(process => process.parent())}
35
+ >
36
+ {children}
37
+ </ContextProvider>
38
+ </SystemProvider>
39
+ ```
98
40
 
99
- function Content() {
100
- const { theme } = useDesktopPreferences()
101
- const appearance = useSystemAppearance()
102
- const desktop = useDesktopSize()
41
+ Each provider accepts explicitly selected sources and resolves only those
42
+ values. A provider requires at least one source; mounting it does not fetch an
43
+ entire runtime implicitly.
103
44
 
104
- return <p style={{ color: appearance.foreground[theme] }}>{desktop.width} × {desktop.height}</p>
105
- }
45
+ ## Hooks
106
46
 
107
- function App() {
108
- return <SystemProvider provide={["appearance", "desktopPreferences", "desktopSize"]} fallback={<p>Loading…</p>}>
109
- <Content />
110
- </SystemProvider>
111
- }
112
- ```
47
+ Context hooks expose the supplied handles:
113
48
 
114
- The selection is required and non-empty. Nothing is read or subscribed merely
115
- because either SDK was imported, and an unselected system value never enters the
116
- Client. `pointerPosition` is permission-guarded: selecting it does not request
117
- permission, and resolution fails unless the Program already holds `pointer`.
118
- The provider renders its optional fallback, or `null`, until every selected
119
- value resolves. `useDesktopPreferences()` is a pure state adapter. The System
120
- communicates its effective scheme through the Client iframe, while each Client
121
- HTML document declares which schemes it supports.
49
+ - `useProgram()`
50
+ - `useProcess()`
51
+ - `useParent()`
122
52
 
123
- ```tsx
124
- import { context } from "@phreshos/client"
125
- import {
126
- ContextProvider,
127
- useProgram,
128
- useSubscribe
129
- } from "@phreshos/react"
130
-
131
- function Counter() {
132
- const program = useProgram()
133
-
134
- const count = useSubscribe(context, "count", message => {
135
- return Number(message.payload)
136
- })
137
-
138
- return <p>{program.name}: {count ?? 0}</p>
139
- }
140
-
141
- export default function App() {
142
- return (
143
- <ContextProvider provide={["program"]} fallback={<p>Loading…</p>} waitServer>
144
- <Counter />
145
- </ContextProvider>
146
- )
147
- }
53
+ System hooks expose the supplied live snapshots:
54
+
55
+ - `useSystemAppearance()`
56
+ - `useDesktopSurface()`
57
+ - `useDesktopPointer()`
58
+ - `useDesktopPreferences()`
59
+
60
+ State hooks compose one explicit read with future events:
61
+
62
+ - `useProgramState()`
63
+ - `useProcessState()`
64
+ - `useServiceState()`
65
+ - `useWindowState()`
66
+
67
+ `useSubscribe()`, `useSubscribeAsks()`, and `useSubscribeAnswers()` own
68
+ their mounted registrations and clean them up on unmount. Unresolved initial
69
+ reads remain `undefined`; the adapters do not invent fallback domain state.
70
+
71
+ ## Development
72
+
73
+ ```sh
74
+ bun install --frozen-lockfile
75
+ bun run verify
148
76
  ```
149
77
 
150
- Using a context or system hook outside its provider, or without selecting its
151
- value, throws a configuration error. Neither provider supplies an implicit
152
- "everything" selection, so adding a future capability cannot make it enter an
153
- existing application.
78
+ `verify` checks the contracts, tests the adapters in React, builds the package,
79
+ and validates its public artifact.
80
+
81
+ ## Repository boundary
82
+
83
+ This repository owns React lifecycle adaptation only. Core owns the contracts,
84
+ runtime SDKs provide the sources, and React UI owns visual components.
85
+
86
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository workflow and
87
+ [SECURITY.md](SECURITY.md) for private vulnerability reporting.
154
88
 
155
- Each hook calls the registration's returned cleanup when the component no
156
- longer consumes it. Projectors may return a value or Promise; only the latest
157
- invocation may update the hook state. Projector failures are never converted
158
- into communication, logged, or suppressed by the SDK; they remain local to the
159
- React application.
89
+ ## License
160
90
 
161
- React is a peer dependency, so this package never installs or bundles a second
162
- copy into an application.
91
+ Licensed under the [MIT License](LICENSE). Copyright © 2026 Zohayr SLILEH.
@@ -1,34 +1,30 @@
1
1
  import { type ReactNode } from "react";
2
- import { type Process, type Program } from "@phreshos/client";
3
- declare const provisionNames: readonly ["program", "process", "parent"];
4
- /** Resolves only the runtime handles explicitly selected by the program. */
5
- export default function ContextProvider({ children, fallback, provide, waitServer }: ContextProviderProperties): import("react").FunctionComponentElement<SelectedContextProviderProperties>;
6
- /** Returns the current Program selected by the nearest provider. */
7
- export declare function useProgram(): Program;
8
- /** Returns the current Process selected by the nearest provider. */
9
- export declare function useProcess(): Process;
10
- /** Returns the current Process's selected visible parent. */
11
- export declare function useParent(): Process | null;
12
- /** Values that ContextProvider can resolve. */
13
- export type ContextProvisionName = typeof provisionNames[number];
14
- /** Required non-empty selection of runtime values. */
15
- export type ContextProvision = readonly [ContextProvisionName, ...ContextProvisionName[]];
2
+ import type { Process, Program } from "@phreshos/core";
3
+ /** Resolves only the runtime handles supplied by the surrounding runtime. */
4
+ export default function ContextProvider({ children, fallback, parent, process, program }: ContextProviderProperties): string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").FunctionComponentElement<import("react").ProviderProps<Partial<Readonly<{
5
+ program: Program;
6
+ process: Process;
7
+ parent: Process | null;
8
+ }>> | null>> | null;
9
+ /** Returns the current Program supplied by the nearest provider. */
10
+ export declare function useProgram<Handle extends Program = Program>(): Handle;
11
+ /** Returns the current Process supplied by the nearest provider. */
12
+ export declare function useProcess<Handle extends Process = Process>(): Handle;
13
+ /** Returns the current Process's supplied visible parent. */
14
+ export declare function useParent<Handle extends Process = Process>(): Handle | null;
15
+ /** A stable handle or resolver supplied by one runtime integration. */
16
+ export type ContextSource<Value> = Value | (() => Value | PromiseLike<Value>);
16
17
  /** Properties accepted by ContextProvider. */
17
- export interface ContextProviderProperties {
18
- /** Content rendered after every selected runtime handle has resolved. */
18
+ export type ContextProviderProperties = Readonly<{
19
19
  readonly children: ReactNode;
20
- /** Content rendered while selected handles or optional Server readiness resolve. */
21
20
  readonly fallback?: ReactNode;
22
- /** Exact runtime values made available to descendant hooks. */
23
- readonly provide: ContextProvision;
24
- /**
25
- * Waits for the Process's Server before rendering children.
26
- * `true` uses the default ten-second deadline; a number sets milliseconds.
27
- */
28
- readonly waitServer?: boolean | number;
29
- }
30
- type SelectedContextProviderProperties = Omit<ContextProviderProperties, "provide" | "waitServer"> & Readonly<{
31
- selection: readonly ContextProvisionName[];
32
- waitServer: boolean | number;
21
+ }> & AtLeastOne<ContextSources>;
22
+ type ContextSources = Readonly<{
23
+ readonly parent?: ContextSource<Process | null>;
24
+ readonly process?: ContextSource<Process>;
25
+ readonly program?: ContextSource<Program>;
33
26
  }>;
27
+ type AtLeastOne<Values> = {
28
+ [Name in keyof Values]-?: Required<Pick<Values, Name>> & Partial<Omit<Values, Name>>;
29
+ }[keyof Values];
34
30
  export {};
@@ -1,93 +1,63 @@
1
1
  import { createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
2
- import { context } from "@phreshos/client";
3
- const provisionNames = ["program", "process", "parent"];
4
2
  const RuntimeContext = createContext(null);
5
- /** Resolves only the runtime handles explicitly selected by the program. */
6
- export default function ContextProvider({ children, fallback = null, provide, waitServer = false }) {
7
- const normalized = normalizeProvision(provide);
8
- const selectionKey = normalized.join("\0");
9
- const selection = useMemo(() => normalized, [selectionKey]);
10
- return createElement(SelectedContextProvider, {
11
- children,
12
- fallback,
13
- key: `${selectionKey}\0${String(waitServer)}`,
14
- selection,
15
- waitServer
16
- });
17
- }
18
- function SelectedContextProvider({ children, fallback, selection, waitServer }) {
19
- const [resolution, setResolution] = useState({ status: "pending" });
3
+ /** Resolves only the runtime handles supplied by the surrounding runtime. */
4
+ export default function ContextProvider({ children, fallback = null, parent, process, program }) {
5
+ const sources = useMemo(() => ({ parent, process, program }), [parent, process, program]);
6
+ const [resolution, setResolution] = useState({ status: "pending", sources });
7
+ if (parent === undefined && process === undefined && program === undefined) {
8
+ throw new Error("ContextProvider requires at least one runtime handle");
9
+ }
20
10
  useEffect(() => {
21
11
  let active = true;
22
- setResolution({ status: "pending" });
23
- void resolveContext(selection, waitServer).then(value => {
12
+ void resolveSources(sources).then(value => {
24
13
  if (active)
25
- setResolution({ status: "ready", value });
14
+ setResolution({ status: "ready", sources, value });
26
15
  }, error => {
27
16
  if (active)
28
- setResolution({ status: "error", error });
17
+ setResolution({ status: "error", sources, error });
29
18
  });
30
19
  return () => { active = false; };
31
- }, [selection, waitServer]);
32
- if (resolution.status === "pending")
20
+ }, [sources]);
21
+ if (resolution.sources !== sources || resolution.status === "pending")
33
22
  return fallback;
34
23
  if (resolution.status === "error")
35
24
  throw resolution.error;
36
25
  return createElement(RuntimeContext.Provider, { value: resolution.value }, children);
37
26
  }
38
- async function resolveContext(selection, waitServer) {
39
- const serverReady = typeof waitServer === "number"
40
- ? context.server.waitReady(waitServer)
41
- : waitServer ? context.server.waitReady() : undefined;
42
- const [entries] = await Promise.all([
43
- Promise.all(selection.map(resolveProvision)),
44
- serverReady
45
- ]);
46
- return {
47
- provided: new Set(selection),
48
- values: Object.fromEntries(entries)
49
- };
27
+ async function resolveSources(sources) {
28
+ const entries = await Promise.all(contextNames.flatMap(name => {
29
+ const source = sources[name];
30
+ return source === undefined ? [] : [resolveSource(name, source)];
31
+ }));
32
+ return Object.fromEntries(entries);
50
33
  }
51
- async function resolveProvision(name) {
52
- switch (name) {
53
- case "program": return [name, await context.program()];
54
- case "process": return [name, await context.process()];
55
- case "parent": return [name, await context.parent()];
56
- }
34
+ async function resolveSource(name, source) {
35
+ const value = typeof source === "function" ? source() : source;
36
+ return [name, await value];
57
37
  }
58
- /** Returns the current Program selected by the nearest provider. */
38
+ /** Returns the current Program supplied by the nearest provider. */
59
39
  export function useProgram() {
60
40
  return useProvided("program");
61
41
  }
62
- /** Returns the current Process selected by the nearest provider. */
42
+ /** Returns the current Process supplied by the nearest provider. */
63
43
  export function useProcess() {
64
44
  return useProvided("process");
65
45
  }
66
- /** Returns the current Process's selected visible parent. */
46
+ /** Returns the current Process's supplied visible parent. */
67
47
  export function useParent() {
68
48
  return useProvided("parent");
69
49
  }
70
50
  function useProvided(name) {
71
- const providedContext = useContext(RuntimeContext);
72
- if (!providedContext)
73
- throw new Error(`use${hookName(name)} must be used inside ContextProvider`);
74
- if (!providedContext.provided.has(name))
75
- throw new Error(`use${hookName(name)} requires "${name}" in ContextProvider's provide prop`);
76
- return providedContext.values[name];
77
- }
78
- function normalizeProvision(provide) {
79
- if (!Array.isArray(provide) || provide.length === 0)
80
- throw new Error("ContextProvider's provide prop must select at least one runtime value");
81
- const selected = new Set();
82
- for (const name of provide) {
83
- if (!provisionNames.includes(name))
84
- throw new Error(`ContextProvider cannot provide "${String(name)}"`);
85
- if (selected.has(name))
86
- throw new Error(`ContextProvider's provide prop selects "${name}" more than once`);
87
- selected.add(name);
88
- }
89
- return [...selected];
90
- }
91
- function hookName(name) {
92
- return name.charAt(0).toUpperCase() + name.slice(1);
93
- }
51
+ const context = useContext(RuntimeContext);
52
+ if (!context)
53
+ throw new Error(`${hookNames[name]} must be used inside ContextProvider`);
54
+ if (!(name in context))
55
+ throw new Error(`${hookNames[name]} requires ContextProvider's ${name} prop`);
56
+ return context[name];
57
+ }
58
+ const contextNames = ["program", "process", "parent"];
59
+ const hookNames = {
60
+ parent: "useParent",
61
+ process: "useProcess",
62
+ program: "useProgram"
63
+ };
@@ -1,4 +1,4 @@
1
- import type { Cleanup } from "@phreshos/client";
1
+ import type { Cleanup } from "@phreshos/core";
2
2
  /** A persistent SDK registration consumed by the React adapter. */
3
3
  export type Connect<Message> = (receive: (message: Message) => void) => Cleanup;
4
4
  /** Retains only the latest projected event value for one mounted hook. */
@@ -1,4 +1,4 @@
1
- import type { Cleanup } from "@phreshos/client";
1
+ import type { Cleanup } from "@phreshos/core";
2
2
  /** One explicitly requested snapshot followed by only future live changes. */
3
3
  export default class LiveSnapshot<Value> {
4
4
  private readonly read;
@@ -1,4 +1,4 @@
1
- import type { Cleanup } from "@phreshos/client";
1
+ import type { Cleanup } from "@phreshos/core";
2
2
  export type StateReducer<State> = (state: State) => State;
3
3
  export type StateFollower<State> = (reduce: (reducer: StateReducer<State>) => void) => Cleanup;
4
4
  /** One mounted React snapshot composed from an explicit read and future events. */
package/dist/main.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { default as ContextProvider, useParent, useProcess, useProgram, type ContextProvision, type ContextProvisionName, type ContextProviderProperties } from "./context-provider.js";
2
- export { default as SystemProvider, usePointerPosition, useDesktopSize, useDesktopPreferences, useSystemAppearance, type SystemProvision, type SystemProvisionName, type SystemProviderProperties } from "./system-provider.js";
1
+ export { default as ContextProvider, useParent, useProcess, useProgram, type ContextSource, type ContextProviderProperties } from "./context-provider.js";
2
+ export { default as SystemProvider, useDesktopPointer, useDesktopSurface, useDesktopPreferences, useSystemAppearance, type SystemProviderProperties } from "./system-provider.js";
3
3
  export { default as useSubscribe } from "./use-subscribe.js";
4
4
  export { default as useProgramState, type ProgramState } from "./use-program-state.js";
5
5
  export { default as useProcessState, type ProcessState } from "./use-process-state.js";
package/dist/main.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { default as ContextProvider, useParent, useProcess, useProgram } from "./context-provider.js";
2
- export { default as SystemProvider, usePointerPosition, useDesktopSize, useDesktopPreferences, useSystemAppearance } from "./system-provider.js";
2
+ export { default as SystemProvider, useDesktopPointer, useDesktopSurface, useDesktopPreferences, useSystemAppearance } from "./system-provider.js";
3
3
  export { default as useSubscribe } from "./use-subscribe.js";
4
4
  export { default as useProgramState } from "./use-program-state.js";
5
5
  export { default as useProcessState } from "./use-process-state.js";
@@ -1,30 +1,38 @@
1
1
  import { type ReactNode } from "react";
2
- import { type Appearance, type DesktopPreferences, type DesktopSize, type PointerPosition } from "@phreshos/client";
3
- declare const provisionNames: readonly ["appearance", "desktopPreferences", "desktopSize", "pointerPosition"];
4
- /** Requests and follows only the system values explicitly selected by the program. */
5
- export default function SystemProvider(properties: SystemProviderProperties): import("react").FunctionComponentElement<SelectedSystemProviderProperties>;
2
+ import type { Appearance, AppearanceSource, DesktopPointerSnapshot, DesktopPointerSource, DesktopPreferences, DesktopPreferencesSource, DesktopSurfaceSnapshot, DesktopSurfaceSource } from "@phreshos/core";
3
+ import LiveSnapshot from "./live-snapshot.js";
4
+ /** Resolves and follows only the System capabilities supplied by the runtime. */
5
+ export default function SystemProvider({ appearance, children, desktopPointer, desktopPreferences, desktopSurface, fallback }: SystemProviderProperties): string | number | bigint | boolean | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").FunctionComponentElement<import("react").ProviderProps<SystemStores | null>> | null;
6
6
  /** Returns the complete unresolved Appearance and follows authoritative updates. */
7
7
  export declare function useSystemAppearance(): Appearance;
8
+ /** Returns the Desktop surface snapshot and follows future resizes. */
9
+ export declare function useDesktopSurface(): DesktopSurfaceSnapshot;
10
+ /** Returns the Desktop pointer snapshot and follows future movement. */
11
+ export declare function useDesktopPointer(): DesktopPointerSnapshot;
8
12
  /** Returns all effective Desktop preferences and follows authoritative updates. */
9
13
  export declare function useDesktopPreferences(): DesktopPreferences;
10
- /** Returns the selected desktop size and follows future resize events. */
11
- export declare function useDesktopSize(): DesktopSize;
12
- /** Returns the selected Pointer position and follows future move events. */
13
- export declare function usePointerPosition(): PointerPosition | null;
14
- /** Values that SystemProvider can request and follow. */
15
- export type SystemProvisionName = typeof provisionNames[number];
16
- /** Required non-empty selection of system values. */
17
- export type SystemProvision = readonly [SystemProvisionName, ...SystemProvisionName[]];
18
14
  /** Properties accepted by SystemProvider. */
19
- export interface SystemProviderProperties {
20
- /** Content rendered after every selected system value has resolved. */
15
+ export type SystemProviderProperties = Readonly<{
21
16
  readonly children: ReactNode;
22
- /** Content rendered while selected system values resolve. */
23
17
  readonly fallback?: ReactNode;
24
- /** Exact system values requested and followed for mounted descendants. */
25
- readonly provide: SystemProvision;
26
- }
27
- type SelectedSystemProviderProperties = SystemProviderProperties & Readonly<{
28
- selection: readonly SystemProvisionName[];
18
+ }> & AtLeastOne<SystemSources>;
19
+ type SystemSources = Readonly<{
20
+ readonly appearance?: AppearanceSource;
21
+ readonly desktopPointer?: DesktopPointerSource;
22
+ readonly desktopPreferences?: DesktopPreferencesSource;
23
+ readonly desktopSurface?: DesktopSurfaceSource;
29
24
  }>;
25
+ type SystemValues = Readonly<{
26
+ appearance: Appearance;
27
+ desktopPointer: DesktopPointerSnapshot;
28
+ desktopPreferences: DesktopPreferences;
29
+ desktopSurface: DesktopSurfaceSnapshot;
30
+ }>;
31
+ type SystemName = keyof SystemValues;
32
+ type SystemStores = {
33
+ [Name in SystemName]?: LiveSnapshot<SystemValues[Name]>;
34
+ };
35
+ type AtLeastOne<Values> = {
36
+ [Name in keyof Values]-?: Required<Pick<Values, Name>> & Partial<Omit<Values, Name>>;
37
+ }[keyof Values];
30
38
  export {};
@@ -1,103 +1,85 @@
1
1
  import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
2
- import { system } from "@phreshos/client";
3
2
  import LiveSnapshot from "./live-snapshot.js";
4
- const provisionNames = ["appearance", "desktopPreferences", "desktopSize", "pointerPosition"];
5
3
  const SystemContext = createContext(null);
6
- /** Requests and follows only the system values explicitly selected by the program. */
7
- export default function SystemProvider(properties) {
8
- const normalized = normalizeProvision(properties.provide);
9
- const selectionKey = normalized.join("\0");
10
- const selection = useMemo(() => normalized, [selectionKey]);
11
- return createElement(SelectedSystemProvider, { ...properties, fallback: properties.fallback ?? null, key: selectionKey, selection });
12
- }
13
- function SelectedSystemProvider({ children, fallback, selection }) {
14
- const stores = useMemo(() => createStores(selection), [selection]);
15
- const [resolution, setResolution] = useState({ status: "pending" });
4
+ /** Resolves and follows only the System capabilities supplied by the runtime. */
5
+ export default function SystemProvider({ appearance, children, desktopPointer, desktopPreferences, desktopSurface, fallback = null }) {
6
+ const stores = useMemo(() => createStores({
7
+ appearance,
8
+ desktopPointer,
9
+ desktopPreferences,
10
+ desktopSurface
11
+ }), [appearance, desktopPointer, desktopPreferences, desktopSurface]);
12
+ const [resolution, setResolution] = useState({ status: "pending", stores });
13
+ if (Object.keys(stores).length === 0)
14
+ throw new Error("SystemProvider requires at least one System capability");
16
15
  useEffect(() => {
17
16
  let active = true;
18
- const selected = selection.map(name => stores[name]);
17
+ const selected = Object.values(stores);
19
18
  void Promise.all(selected.map(store => store.start())).then(() => {
20
19
  if (active)
21
- setResolution({ status: "ready" });
20
+ setResolution({ status: "ready", stores });
22
21
  }, error => {
23
22
  for (const store of selected)
24
23
  store.stop();
25
24
  if (active)
26
- setResolution({ status: "error", error });
25
+ setResolution({ status: "error", stores, error });
27
26
  });
28
27
  return () => {
29
28
  active = false;
30
29
  for (const store of selected)
31
30
  store.stop();
32
31
  };
33
- }, [selection, stores]);
34
- if (resolution.status === "pending")
32
+ }, [stores]);
33
+ if (resolution.stores !== stores || resolution.status === "pending")
35
34
  return fallback;
36
35
  if (resolution.status === "error")
37
36
  throw resolution.error;
38
- return createElement(SystemContext.Provider, { value: { provided: new Set(selection), stores } }, children);
37
+ return createElement(SystemContext.Provider, { value: stores }, children);
39
38
  }
40
39
  /** Returns the complete unresolved Appearance and follows authoritative updates. */
41
40
  export function useSystemAppearance() {
42
41
  return useProvided("appearance");
43
42
  }
43
+ /** Returns the Desktop surface snapshot and follows future resizes. */
44
+ export function useDesktopSurface() {
45
+ return useProvided("desktopSurface");
46
+ }
47
+ /** Returns the Desktop pointer snapshot and follows future movement. */
48
+ export function useDesktopPointer() {
49
+ return useProvided("desktopPointer");
50
+ }
44
51
  /** Returns all effective Desktop preferences and follows authoritative updates. */
45
52
  export function useDesktopPreferences() {
46
53
  return useProvided("desktopPreferences");
47
54
  }
48
- /** Returns the selected desktop size and follows future resize events. */
49
- export function useDesktopSize() {
50
- return useProvided("desktopSize");
51
- }
52
- /** Returns the selected Pointer position and follows future move events. */
53
- export function usePointerPosition() {
54
- return useProvided("pointerPosition");
55
- }
56
55
  function useProvided(name) {
57
56
  const context = useContext(SystemContext);
58
57
  if (!context)
59
58
  throw new Error(`${hookNames[name]} must be used inside SystemProvider`);
60
- if (!context.provided.has(name))
61
- throw new Error(`${hookNames[name]} requires "${name}" in SystemProvider's provide prop`);
62
- const store = context.stores[name];
59
+ const store = context[name];
60
+ if (!store)
61
+ throw new Error(`${hookNames[name]} requires SystemProvider's ${name} prop`);
63
62
  return useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
64
63
  }
65
- function createStores(selection) {
64
+ function createStores(sources) {
66
65
  const stores = {};
67
- for (const name of selection) {
68
- switch (name) {
69
- case "appearance":
70
- stores.appearance = new LiveSnapshot(() => system.appearance.snapshot(), subscriber => system.appearance.subscribe("change", subscriber));
71
- break;
72
- case "desktopPreferences":
73
- stores.desktopPreferences = new LiveSnapshot(() => system.desktopPreferences.snapshot(), subscriber => system.desktopPreferences.subscribe("change", subscriber));
74
- break;
75
- case "desktopSize":
76
- stores.desktopSize = new LiveSnapshot(() => system.desktop.size(), subscriber => system.desktop.subscribe("resize", subscriber));
77
- break;
78
- case "pointerPosition":
79
- stores.pointerPosition = new LiveSnapshot(() => system.pointer.position(), subscriber => system.pointer.subscribe("move", subscriber));
80
- break;
81
- }
82
- }
66
+ const appearance = sources.appearance;
67
+ const surface = sources.desktopSurface;
68
+ const pointer = sources.desktopPointer;
69
+ const preferences = sources.desktopPreferences;
70
+ if (appearance)
71
+ stores.appearance = new LiveSnapshot(() => appearance.snapshot(), subscriber => appearance.subscribe("change", subscriber));
72
+ if (surface)
73
+ stores.desktopSurface = new LiveSnapshot(() => surface.snapshot(), subscriber => surface.subscribe("resize", subscriber));
74
+ if (pointer)
75
+ stores.desktopPointer = new LiveSnapshot(() => pointer.snapshot(), subscriber => pointer.subscribe("move", subscriber));
76
+ if (preferences)
77
+ stores.desktopPreferences = new LiveSnapshot(() => preferences.snapshot(), subscriber => preferences.subscribe("change", subscriber));
83
78
  return stores;
84
79
  }
85
- function normalizeProvision(provide) {
86
- if (!Array.isArray(provide) || provide.length === 0)
87
- throw new Error("SystemProvider's provide prop must select at least one system value");
88
- const selected = new Set();
89
- for (const name of provide) {
90
- if (!provisionNames.includes(name))
91
- throw new Error(`SystemProvider cannot provide "${String(name)}"`);
92
- if (selected.has(name))
93
- throw new Error(`SystemProvider's provide prop selects "${name}" more than once`);
94
- selected.add(name);
95
- }
96
- return [...selected];
97
- }
98
80
  const hookNames = {
99
81
  appearance: "useSystemAppearance",
82
+ desktopPointer: "useDesktopPointer",
100
83
  desktopPreferences: "useDesktopPreferences",
101
- desktopSize: "useDesktopSize",
102
- pointerPosition: "usePointerPosition"
84
+ desktopSurface: "useDesktopSurface"
103
85
  };
@@ -1,4 +1,4 @@
1
- import type { Process } from "@phreshos/client";
1
+ import type { Process } from "@phreshos/core";
2
2
  /** Mutable lifecycle state of one Process and its Endpoint incarnations. */
3
3
  export type ProcessState = Readonly<{
4
4
  exited: boolean;
@@ -1,4 +1,4 @@
1
- import type { Process, Program } from "@phreshos/client";
1
+ import type { Process, Program } from "@phreshos/core";
2
2
  /** Mutable runtime state derived from one Program's reads and live events. */
3
3
  export type ProgramState = Readonly<{
4
4
  installed: boolean;
@@ -1,4 +1,4 @@
1
- import type { Service } from "@phreshos/client";
1
+ import type { Service } from "@phreshos/core";
2
2
  /** Live existence of one configured Endpoint service. */
3
3
  export type ServiceState = Readonly<{
4
4
  exists: boolean;
@@ -1,4 +1,4 @@
1
- import type { AnswerCapture, AnswerSubscriber, Cleanup } from "@phreshos/client";
1
+ import type { AnswerCapture, AnswerSubscriber, Cleanup } from "@phreshos/core";
2
2
  /** Any Server traffic surface capable of subscribing to its outgoing answers. */
3
3
  export type AnswerSubscribable = Readonly<{
4
4
  /** Subscribes to answers sent by the target Server and returns its cleanup. */
@@ -1,4 +1,4 @@
1
- import type { AskCapture, AskSubscriber, Cleanup } from "@phreshos/client";
1
+ import type { AskCapture, AskSubscriber, Cleanup } from "@phreshos/core";
2
2
  /** Any Endpoint traffic surface capable of subscribing to its outgoing questions. */
3
3
  export type AskSubscribable = Readonly<{
4
4
  /** Subscribes to questions sent by the target Endpoint and returns its cleanup. */
@@ -1,4 +1,4 @@
1
- import type { Captures, Context, ContextMessage, Endpoint, EventMessage, EventName, Subscribable, SubscribableEvents, SubscribableFallback } from "@phreshos/client";
1
+ import type { Captures, Context, ContextMessage, Endpoint, EventMessage, EventName, Subscribable, SubscribableEvents, SubscribableFallback } from "@phreshos/core";
2
2
  type OpenEvent = string & {};
3
3
  type AvailableEvent<Events extends object, Fallback> = EventName<Events> | ([Fallback] extends [never] ? never : OpenEvent);
4
4
  type CompatibleEvent<Events extends object, Fallback, Narrowed> = {
@@ -1,3 +1,3 @@
1
- import type { Window, WindowState } from "@phreshos/client";
1
+ import type { Window, WindowState } from "@phreshos/core";
2
2
  /** Explicitly reads and follows one live Client Window while mounted. */
3
3
  export default function useWindowState(window: Window): WindowState | undefined;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@phreshos/react",
3
- "version": "0.1.16",
4
- "description": "React adapters for the Client SDK.",
3
+ "version": "0.1.17",
4
+ "description": "Runtime-neutral React adapters for PhreshOS contracts.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
7
7
  "types": "dist/main.d.ts",
@@ -47,13 +47,11 @@
47
47
  "prepack": "node --run test && node --run build"
48
48
  },
49
49
  "peerDependencies": {
50
- "@phreshos/client": "^0.1.28",
51
- "@phreshos/core": "^0.1.29",
50
+ "@phreshos/core": "^0.1.31",
52
51
  "react": "^19.2.0"
53
52
  },
54
53
  "devDependencies": {
55
- "@phreshos/client": "^0.1.28",
56
- "@phreshos/core": "^0.1.29",
54
+ "@phreshos/core": "^0.1.31",
57
55
  "@testing-library/react": "^16.3.0",
58
56
  "@types/react": "^19.2.18",
59
57
  "@types/react-dom": "^19.2.4",