@phreshos/react 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +99 -5
- package/dist/current-provider.d.ts +21 -21
- package/dist/current-provider.js +60 -31
- package/dist/host-provider.d.ts +28 -0
- package/dist/host-provider.js +95 -0
- package/dist/live-snapshot.d.ts +18 -0
- package/dist/live-snapshot.js +59 -0
- package/dist/live-state.d.ts +24 -0
- package/dist/live-state.js +95 -0
- package/dist/main.d.ts +7 -1
- package/dist/main.js +7 -1
- package/dist/use-color.d.ts +3 -0
- package/dist/use-color.js +6 -0
- package/dist/use-observe-answers.d.ts +1 -0
- package/dist/use-observe-asks.d.ts +1 -0
- package/dist/use-process-state.d.ts +9 -0
- package/dist/use-process-state.js +23 -0
- package/dist/use-program-state.d.ts +8 -0
- package/dist/use-program-state.js +22 -0
- package/dist/use-scale.d.ts +3 -0
- package/dist/use-scale.js +6 -0
- package/dist/use-subscribe.d.ts +13 -3
- package/dist/use-subscribe.js +4 -1
- package/dist/use-window-state.d.ts +3 -0
- package/dist/use-window-state.js +18 -0
- package/package.json +40 -7
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Zohayr SLILEH
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -16,16 +16,105 @@ dependencies.
|
|
|
16
16
|
|
|
17
17
|
It provides two kinds of adapter:
|
|
18
18
|
|
|
19
|
-
- `CurrentProvider` resolves the
|
|
20
|
-
|
|
21
|
-
`useProcess()`,
|
|
19
|
+
- `CurrentProvider` resolves exactly the current values named by its required
|
|
20
|
+
`provide` prop, then exposes them synchronously through `useProgram()`,
|
|
21
|
+
`useProcess()`, and `useParent()`.
|
|
22
|
+
- `HostProvider` subscribes before reading exactly the host values named by its
|
|
23
|
+
required `provide` prop. `useHostTheme()`, `useSurfaceSize()`, and
|
|
24
|
+
`usePointerPosition()` expose those values synchronously after resolution.
|
|
22
25
|
- `useSubscribe()` and `useObserve()` own persistent ordinary-event
|
|
23
26
|
registrations for one mounted React consumer.
|
|
27
|
+
- `useProgramState(program)`, `useProcessState(process)`, and
|
|
28
|
+
`useWindowState(window)` explicitly compose existing reads with future live
|
|
29
|
+
events for one mounted consumer. They add no state operation to another SDK.
|
|
24
30
|
- `useObserveAsks()` adapts an Endpoint traffic surface's question observation,
|
|
25
31
|
while `useObserveAnswers()` adapts a Server traffic surface's answer
|
|
26
32
|
observation.
|
|
33
|
+
- `useScale(value)` and `useColor(value)` memoize the complete derived scale
|
|
34
|
+
around the explicit number or CSS color supplied by the component. They do
|
|
35
|
+
not read a Theme or choose a source value implicitly.
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
Window needs no React resolution: `current.window` is already a synchronous,
|
|
38
|
+
silent capability object. `CurrentProvider` therefore has no `window` selection
|
|
39
|
+
and the React SDK exposes no pass-through `useWindow()` hook.
|
|
40
|
+
|
|
41
|
+
The domain state hooks return only mutable state; identity and immutable
|
|
42
|
+
metadata remain on the supplied handle:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
useProgramState(program)
|
|
46
|
+
// { installed, processes } | undefined
|
|
47
|
+
|
|
48
|
+
useProcessState(process)
|
|
49
|
+
// { exited, serverExists, clientExists } | undefined
|
|
50
|
+
|
|
51
|
+
useWindowState(window)
|
|
52
|
+
// WindowState | undefined
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`WindowState` contains the observable Window properties only. The command-only
|
|
56
|
+
`window.surface` capability is intentionally absent: Program code may replace
|
|
57
|
+
or remove its target but cannot read or subscribe to it.
|
|
58
|
+
|
|
59
|
+
`undefined` means only that the initial explicit reads are pending. Each hook
|
|
60
|
+
opens its live subscriptions before those reads, then applies any intervening
|
|
61
|
+
events so an older result cannot overwrite newer state. If an initial read
|
|
62
|
+
rejects, the hook throws that original value during render for the nearest
|
|
63
|
+
React error boundary. It does not invent a fallback, retry, wait for a stopped
|
|
64
|
+
Client, or use an old Window snapshot as the fallback for that rejection. State
|
|
65
|
+
exists only while the hook is mounted and every registration is cleaned up on
|
|
66
|
+
unmount.
|
|
67
|
+
|
|
68
|
+
The Window capability outlives a stopped Client, but live Window state does
|
|
69
|
+
not. A component that spans Client lifecycle should use `useProcessState()` to
|
|
70
|
+
mount its `useWindowState()` child only while `clientExists` is true. Calling
|
|
71
|
+
the Window hook while the Client is absent lets the existing Window reads
|
|
72
|
+
reject normally.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
const theme = useHostTheme()
|
|
76
|
+
const spacing = useScale(theme.spacing)
|
|
77
|
+
const radius = useScale(theme.radius)
|
|
78
|
+
const accent = useColor(theme.accent)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The ordinary-event and traffic hooks retain the latest value returned by their projector.
|
|
82
|
+
`useSubscribe()` may omit that callback; its default projector is
|
|
83
|
+
`message => message`, so the hook retains the latest message unchanged.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { host } from "@phreshos/client"
|
|
87
|
+
import { useSubscribe } from "@phreshos/react"
|
|
88
|
+
|
|
89
|
+
const surface = useSubscribe(host.surface, "resize")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Host reads are asynchronous while subscriptions are live-only. `HostProvider`
|
|
93
|
+
subscribes before requesting each selected snapshot and prevents an older read
|
|
94
|
+
from overwriting a newer event:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { HostProvider, useHostTheme, useSurfaceSize } from "@phreshos/react"
|
|
98
|
+
|
|
99
|
+
function Content() {
|
|
100
|
+
const theme = useHostTheme()
|
|
101
|
+
const surface = useSurfaceSize()
|
|
102
|
+
|
|
103
|
+
return <p style={{ color: theme.foreground }}>{surface.width} × {surface.height}</p>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function App() {
|
|
107
|
+
return <HostProvider provide={["theme", "surfaceSize"]} fallback={<p>Loading…</p>}>
|
|
108
|
+
<Content />
|
|
109
|
+
</HostProvider>
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The selection is required and non-empty. Nothing is read or subscribed merely
|
|
114
|
+
because either SDK was imported, and an unselected host value never enters the
|
|
115
|
+
Client. `pointerPosition` is permission-guarded: selecting it does not request
|
|
116
|
+
permission, and resolution fails unless the Program already holds `pointer`.
|
|
117
|
+
The provider renders its fallback until every selected value resolves.
|
|
29
118
|
|
|
30
119
|
```tsx
|
|
31
120
|
import { current } from "@phreshos/client"
|
|
@@ -47,13 +136,18 @@ function Counter() {
|
|
|
47
136
|
|
|
48
137
|
export default function App() {
|
|
49
138
|
return (
|
|
50
|
-
<CurrentProvider fallback={<p>Loading…</p>} waitServer>
|
|
139
|
+
<CurrentProvider provide={["program"]} fallback={<p>Loading…</p>} waitServer>
|
|
51
140
|
<Counter />
|
|
52
141
|
</CurrentProvider>
|
|
53
142
|
)
|
|
54
143
|
}
|
|
55
144
|
```
|
|
56
145
|
|
|
146
|
+
Using a current or host hook outside its provider, or without selecting its
|
|
147
|
+
value, throws a configuration error. Neither provider supplies an implicit
|
|
148
|
+
"everything" selection, so adding a future capability cannot make it enter an
|
|
149
|
+
existing application.
|
|
150
|
+
|
|
57
151
|
Each hook calls the registration's returned cleanup when the component no
|
|
58
152
|
longer consumes it. Projectors may return a value or Promise; only the latest
|
|
59
153
|
invocation may update the hook state. Projector failures are never converted
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import { type Process, type Program
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* navigation into synchronous React context for the hooks below it.
|
|
8
|
-
*/
|
|
9
|
-
export default function CurrentProvider({ children, fallback, waitServer }: CurrentProviderProperties): 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<Readonly<{
|
|
10
|
-
program: Program;
|
|
11
|
-
process: Process;
|
|
12
|
-
parent: Process | null;
|
|
13
|
-
window: Window;
|
|
14
|
-
}> | null>> | null | undefined;
|
|
15
|
-
/** Returns the current Program from the nearest {@link CurrentProvider}. */
|
|
2
|
+
import { type Process, type Program } from "@phreshos/client";
|
|
3
|
+
declare const provisionNames: readonly ["program", "process", "parent"];
|
|
4
|
+
/** Resolves only the current handles explicitly selected by the program. */
|
|
5
|
+
export default function CurrentProvider({ children, fallback, provide, waitServer }: CurrentProviderProperties): import("react").FunctionComponentElement<SelectedCurrentProviderProperties>;
|
|
6
|
+
/** Returns the current Program selected by the nearest provider. */
|
|
16
7
|
export declare function useProgram(): Program;
|
|
17
|
-
/** Returns the current Process
|
|
8
|
+
/** Returns the current Process selected by the nearest provider. */
|
|
18
9
|
export declare function useProcess(): Process;
|
|
19
|
-
/** Returns the current Process's visible parent
|
|
10
|
+
/** Returns the current Process's selected visible parent. */
|
|
20
11
|
export declare function useParent(): Process | null;
|
|
21
|
-
/**
|
|
22
|
-
export
|
|
23
|
-
/**
|
|
12
|
+
/** Values that CurrentProvider can resolve. */
|
|
13
|
+
export type CurrentProvisionName = typeof provisionNames[number];
|
|
14
|
+
/** Required non-empty selection of current values. */
|
|
15
|
+
export type CurrentProvision = readonly [CurrentProvisionName, ...CurrentProvisionName[]];
|
|
16
|
+
/** Properties accepted by CurrentProvider. */
|
|
24
17
|
export interface CurrentProviderProperties {
|
|
25
|
-
/** Content rendered after every current handle has resolved. */
|
|
18
|
+
/** Content rendered after every selected current handle has resolved. */
|
|
26
19
|
readonly children: ReactNode;
|
|
27
|
-
/** Content rendered while
|
|
20
|
+
/** Content rendered while selected handles or optional Server readiness resolve. */
|
|
28
21
|
readonly fallback: ReactNode;
|
|
22
|
+
/** Exact current values made available to descendant hooks. */
|
|
23
|
+
readonly provide: CurrentProvision;
|
|
29
24
|
/**
|
|
30
25
|
* Waits for the Process's Server before rendering children.
|
|
31
26
|
* `true` uses the default ten-second deadline; a number sets milliseconds.
|
|
32
27
|
*/
|
|
33
28
|
readonly waitServer?: boolean | number;
|
|
34
29
|
}
|
|
30
|
+
type SelectedCurrentProviderProperties = Omit<CurrentProviderProperties, "provide" | "waitServer"> & Readonly<{
|
|
31
|
+
selection: readonly CurrentProvisionName[];
|
|
32
|
+
waitServer: boolean | number;
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|
package/dist/current-provider.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
|
-
import { createContext, createElement, useContext, useEffect, useState } from "react";
|
|
1
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import { current } from "@phreshos/client";
|
|
3
|
+
const provisionNames = ["program", "process", "parent"];
|
|
3
4
|
const CurrentContext = createContext(null);
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/** Resolves only the current handles explicitly selected by the program. */
|
|
6
|
+
export default function CurrentProvider({ children, fallback, provide, waitServer = false }) {
|
|
7
|
+
const normalized = normalizeProvision(provide);
|
|
8
|
+
const selectionKey = normalized.join("\0");
|
|
9
|
+
const selection = useMemo(() => normalized, [selectionKey]);
|
|
10
|
+
return createElement(SelectedCurrentProvider, {
|
|
11
|
+
children,
|
|
12
|
+
fallback,
|
|
13
|
+
key: `${selectionKey}\0${String(waitServer)}`,
|
|
14
|
+
selection,
|
|
15
|
+
waitServer
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function SelectedCurrentProvider({ children, fallback, selection, waitServer }) {
|
|
11
19
|
const [resolution, setResolution] = useState({ status: "pending" });
|
|
12
20
|
useEffect(() => {
|
|
13
21
|
let active = true;
|
|
14
22
|
setResolution({ status: "pending" });
|
|
15
|
-
void resolveCurrent(waitServer).then(value => {
|
|
23
|
+
void resolveCurrent(selection, waitServer).then(value => {
|
|
16
24
|
if (active)
|
|
17
25
|
setResolution({ status: "ready", value });
|
|
18
26
|
}, error => {
|
|
@@ -20,45 +28,66 @@ export default function CurrentProvider({ children, fallback, waitServer = false
|
|
|
20
28
|
setResolution({ status: "error", error });
|
|
21
29
|
});
|
|
22
30
|
return () => { active = false; };
|
|
23
|
-
}, [waitServer]);
|
|
31
|
+
}, [selection, waitServer]);
|
|
24
32
|
if (resolution.status === "pending")
|
|
25
33
|
return fallback;
|
|
26
34
|
if (resolution.status === "error")
|
|
27
35
|
throw resolution.error;
|
|
28
36
|
return createElement(CurrentContext.Provider, { value: resolution.value }, children);
|
|
29
37
|
}
|
|
30
|
-
async function resolveCurrent(waitServer) {
|
|
38
|
+
async function resolveCurrent(selection, waitServer) {
|
|
31
39
|
const serverReady = typeof waitServer === "number"
|
|
32
40
|
? current.server.waitReady(waitServer)
|
|
33
41
|
: waitServer ? current.server.waitReady() : undefined;
|
|
34
|
-
const [
|
|
35
|
-
|
|
36
|
-
current.process(),
|
|
37
|
-
current.parent(),
|
|
42
|
+
const [entries] = await Promise.all([
|
|
43
|
+
Promise.all(selection.map(resolveProvision)),
|
|
38
44
|
serverReady
|
|
39
45
|
]);
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
return {
|
|
47
|
+
provided: new Set(selection),
|
|
48
|
+
values: Object.fromEntries(entries)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function resolveProvision(name) {
|
|
52
|
+
switch (name) {
|
|
53
|
+
case "program": return [name, await current.program()];
|
|
54
|
+
case "process": return [name, await current.process()];
|
|
55
|
+
case "parent": return [name, await current.parent()];
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
|
-
/** Returns the current Program
|
|
58
|
+
/** Returns the current Program selected by the nearest provider. */
|
|
44
59
|
export function useProgram() {
|
|
45
|
-
return
|
|
60
|
+
return useProvided("program");
|
|
46
61
|
}
|
|
47
|
-
/** Returns the current Process
|
|
62
|
+
/** Returns the current Process selected by the nearest provider. */
|
|
48
63
|
export function useProcess() {
|
|
49
|
-
return
|
|
64
|
+
return useProvided("process");
|
|
50
65
|
}
|
|
51
|
-
/** Returns the current Process's visible parent
|
|
66
|
+
/** Returns the current Process's selected visible parent. */
|
|
52
67
|
export function useParent() {
|
|
53
|
-
return
|
|
68
|
+
return useProvided("parent");
|
|
69
|
+
}
|
|
70
|
+
function useProvided(name) {
|
|
71
|
+
const context = useContext(CurrentContext);
|
|
72
|
+
if (!context)
|
|
73
|
+
throw new Error(`use${hookName(name)} must be used inside CurrentProvider`);
|
|
74
|
+
if (!context.provided.has(name))
|
|
75
|
+
throw new Error(`use${hookName(name)} requires "${name}" in CurrentProvider's provide prop`);
|
|
76
|
+
return context.values[name];
|
|
54
77
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
function normalizeProvision(provide) {
|
|
79
|
+
if (!Array.isArray(provide) || provide.length === 0)
|
|
80
|
+
throw new Error("CurrentProvider's provide prop must select at least one current value");
|
|
81
|
+
const selected = new Set();
|
|
82
|
+
for (const name of provide) {
|
|
83
|
+
if (!provisionNames.includes(name))
|
|
84
|
+
throw new Error(`CurrentProvider cannot provide "${String(name)}"`);
|
|
85
|
+
if (selected.has(name))
|
|
86
|
+
throw new Error(`CurrentProvider's provide prop selects "${name}" more than once`);
|
|
87
|
+
selected.add(name);
|
|
88
|
+
}
|
|
89
|
+
return [...selected];
|
|
58
90
|
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
if (!value)
|
|
62
|
-
throw new Error("Current hooks must be used inside CurrentProvider");
|
|
63
|
-
return value;
|
|
91
|
+
function hookName(name) {
|
|
92
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
64
93
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type PointerPosition, type Surface, type ThemeProperties } from "@phreshos/client";
|
|
3
|
+
declare const provisionNames: readonly ["theme", "surfaceSize", "pointerPosition"];
|
|
4
|
+
/** Requests and follows only the host values explicitly selected by the program. */
|
|
5
|
+
export default function HostProvider(properties: HostProviderProperties): import("react").FunctionComponentElement<SelectedHostProviderProperties>;
|
|
6
|
+
/** Returns the selected Theme snapshot and follows future change events. */
|
|
7
|
+
export declare function useHostTheme(): Readonly<ThemeProperties>;
|
|
8
|
+
/** Returns the selected Surface size and follows future resize events. */
|
|
9
|
+
export declare function useSurfaceSize(): Surface;
|
|
10
|
+
/** Returns the selected Pointer position and follows future move events. */
|
|
11
|
+
export declare function usePointerPosition(): PointerPosition | null;
|
|
12
|
+
/** Values that HostProvider can request and follow. */
|
|
13
|
+
export type HostProvisionName = typeof provisionNames[number];
|
|
14
|
+
/** Required non-empty selection of host values. */
|
|
15
|
+
export type HostProvision = readonly [HostProvisionName, ...HostProvisionName[]];
|
|
16
|
+
/** Properties accepted by HostProvider. */
|
|
17
|
+
export interface HostProviderProperties {
|
|
18
|
+
/** Content rendered after every selected host value has resolved. */
|
|
19
|
+
readonly children: ReactNode;
|
|
20
|
+
/** Content rendered while selected host values resolve. */
|
|
21
|
+
readonly fallback: ReactNode;
|
|
22
|
+
/** Exact host values requested and followed for mounted descendants. */
|
|
23
|
+
readonly provide: HostProvision;
|
|
24
|
+
}
|
|
25
|
+
type SelectedHostProviderProperties = HostProviderProperties & Readonly<{
|
|
26
|
+
selection: readonly HostProvisionName[];
|
|
27
|
+
}>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createContext, createElement, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { host } from "@phreshos/client";
|
|
3
|
+
import LiveSnapshot from "./live-snapshot.js";
|
|
4
|
+
const provisionNames = ["theme", "surfaceSize", "pointerPosition"];
|
|
5
|
+
const HostContext = createContext(null);
|
|
6
|
+
/** Requests and follows only the host values explicitly selected by the program. */
|
|
7
|
+
export default function HostProvider(properties) {
|
|
8
|
+
const normalized = normalizeProvision(properties.provide);
|
|
9
|
+
const selectionKey = normalized.join("\0");
|
|
10
|
+
const selection = useMemo(() => normalized, [selectionKey]);
|
|
11
|
+
return createElement(SelectedHostProvider, { ...properties, key: selectionKey, selection });
|
|
12
|
+
}
|
|
13
|
+
function SelectedHostProvider({ children, fallback, selection }) {
|
|
14
|
+
const stores = useMemo(() => createStores(selection), [selection]);
|
|
15
|
+
const [resolution, setResolution] = useState({ status: "pending" });
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
let active = true;
|
|
18
|
+
const selected = selection.map(name => stores[name]);
|
|
19
|
+
void Promise.all(selected.map(store => store.start())).then(() => {
|
|
20
|
+
if (active)
|
|
21
|
+
setResolution({ status: "ready" });
|
|
22
|
+
}, error => {
|
|
23
|
+
for (const store of selected)
|
|
24
|
+
store.stop();
|
|
25
|
+
if (active)
|
|
26
|
+
setResolution({ status: "error", error });
|
|
27
|
+
});
|
|
28
|
+
return () => {
|
|
29
|
+
active = false;
|
|
30
|
+
for (const store of selected)
|
|
31
|
+
store.stop();
|
|
32
|
+
};
|
|
33
|
+
}, [selection, stores]);
|
|
34
|
+
if (resolution.status === "pending")
|
|
35
|
+
return fallback;
|
|
36
|
+
if (resolution.status === "error")
|
|
37
|
+
throw resolution.error;
|
|
38
|
+
return createElement(HostContext.Provider, { value: { provided: new Set(selection), stores } }, children);
|
|
39
|
+
}
|
|
40
|
+
/** Returns the selected Theme snapshot and follows future change events. */
|
|
41
|
+
export function useHostTheme() {
|
|
42
|
+
return useProvided("theme");
|
|
43
|
+
}
|
|
44
|
+
/** Returns the selected Surface size and follows future resize events. */
|
|
45
|
+
export function useSurfaceSize() {
|
|
46
|
+
return useProvided("surfaceSize");
|
|
47
|
+
}
|
|
48
|
+
/** Returns the selected Pointer position and follows future move events. */
|
|
49
|
+
export function usePointerPosition() {
|
|
50
|
+
return useProvided("pointerPosition");
|
|
51
|
+
}
|
|
52
|
+
function useProvided(name) {
|
|
53
|
+
const context = useContext(HostContext);
|
|
54
|
+
if (!context)
|
|
55
|
+
throw new Error(`${hookNames[name]} must be used inside HostProvider`);
|
|
56
|
+
if (!context.provided.has(name))
|
|
57
|
+
throw new Error(`${hookNames[name]} requires "${name}" in HostProvider's provide prop`);
|
|
58
|
+
const store = context.stores[name];
|
|
59
|
+
return useSyncExternalStore(store.subscribe, store.snapshot, store.snapshot);
|
|
60
|
+
}
|
|
61
|
+
function createStores(selection) {
|
|
62
|
+
const stores = {};
|
|
63
|
+
for (const name of selection) {
|
|
64
|
+
switch (name) {
|
|
65
|
+
case "theme":
|
|
66
|
+
stores.theme = new LiveSnapshot(() => host.theme.snapshot(), subscriber => host.theme.subscribe("change", subscriber));
|
|
67
|
+
break;
|
|
68
|
+
case "surfaceSize":
|
|
69
|
+
stores.surfaceSize = new LiveSnapshot(() => host.surface.size(), subscriber => host.surface.subscribe("resize", subscriber));
|
|
70
|
+
break;
|
|
71
|
+
case "pointerPosition":
|
|
72
|
+
stores.pointerPosition = new LiveSnapshot(() => host.pointer.position(), subscriber => host.pointer.subscribe("move", subscriber));
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return stores;
|
|
77
|
+
}
|
|
78
|
+
function normalizeProvision(provide) {
|
|
79
|
+
if (!Array.isArray(provide) || provide.length === 0)
|
|
80
|
+
throw new Error("HostProvider's provide prop must select at least one host value");
|
|
81
|
+
const selected = new Set();
|
|
82
|
+
for (const name of provide) {
|
|
83
|
+
if (!provisionNames.includes(name))
|
|
84
|
+
throw new Error(`HostProvider cannot provide "${String(name)}"`);
|
|
85
|
+
if (selected.has(name))
|
|
86
|
+
throw new Error(`HostProvider's provide prop selects "${name}" more than once`);
|
|
87
|
+
selected.add(name);
|
|
88
|
+
}
|
|
89
|
+
return [...selected];
|
|
90
|
+
}
|
|
91
|
+
const hookNames = {
|
|
92
|
+
theme: "useHostTheme",
|
|
93
|
+
surfaceSize: "useSurfaceSize",
|
|
94
|
+
pointerPosition: "usePointerPosition"
|
|
95
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Cleanup } from "@phreshos/client";
|
|
2
|
+
/** One explicitly requested snapshot followed by only future live changes. */
|
|
3
|
+
export default class LiveSnapshot<Value> {
|
|
4
|
+
private readonly read;
|
|
5
|
+
private readonly subscribeSource;
|
|
6
|
+
private readonly listeners;
|
|
7
|
+
private value;
|
|
8
|
+
private stopSource;
|
|
9
|
+
private generation;
|
|
10
|
+
private revision;
|
|
11
|
+
constructor(read: () => Promise<Value>, subscribeSource: (subscriber: (value: Value) => void) => Cleanup);
|
|
12
|
+
readonly snapshot: () => Value;
|
|
13
|
+
readonly subscribe: (listener: () => void) => Cleanup;
|
|
14
|
+
/** Starts the live subscription before requesting the current snapshot. */
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
stop(): void;
|
|
17
|
+
private update;
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const unavailable = Symbol("unavailable");
|
|
2
|
+
/** One explicitly requested snapshot followed by only future live changes. */
|
|
3
|
+
export default class LiveSnapshot {
|
|
4
|
+
read;
|
|
5
|
+
subscribeSource;
|
|
6
|
+
listeners = new Set();
|
|
7
|
+
value = unavailable;
|
|
8
|
+
stopSource = null;
|
|
9
|
+
generation = 0;
|
|
10
|
+
revision = 0;
|
|
11
|
+
constructor(read, subscribeSource) {
|
|
12
|
+
this.read = read;
|
|
13
|
+
this.subscribeSource = subscribeSource;
|
|
14
|
+
}
|
|
15
|
+
snapshot = () => {
|
|
16
|
+
if (this.value === unavailable)
|
|
17
|
+
throw new Error("The requested host value is not ready");
|
|
18
|
+
return this.value;
|
|
19
|
+
};
|
|
20
|
+
subscribe = (listener) => {
|
|
21
|
+
this.listeners.add(listener);
|
|
22
|
+
return () => { this.listeners.delete(listener); };
|
|
23
|
+
};
|
|
24
|
+
/** Starts the live subscription before requesting the current snapshot. */
|
|
25
|
+
async start() {
|
|
26
|
+
this.stop();
|
|
27
|
+
this.value = unavailable;
|
|
28
|
+
const generation = this.generation;
|
|
29
|
+
this.stopSource = this.subscribeSource(value => {
|
|
30
|
+
if (this.generation !== generation)
|
|
31
|
+
return;
|
|
32
|
+
this.revision++;
|
|
33
|
+
this.update(value);
|
|
34
|
+
});
|
|
35
|
+
const requestedAt = this.revision;
|
|
36
|
+
try {
|
|
37
|
+
const value = await this.read();
|
|
38
|
+
if (this.generation === generation && this.revision === requestedAt)
|
|
39
|
+
this.update(value);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (this.generation === generation && this.revision === requestedAt)
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
stop() {
|
|
47
|
+
this.generation++;
|
|
48
|
+
this.revision = 0;
|
|
49
|
+
this.stopSource?.();
|
|
50
|
+
this.stopSource = null;
|
|
51
|
+
}
|
|
52
|
+
update(value) {
|
|
53
|
+
if (Object.is(this.value, value))
|
|
54
|
+
return;
|
|
55
|
+
this.value = value;
|
|
56
|
+
for (const listener of this.listeners)
|
|
57
|
+
listener();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Cleanup } from "@phreshos/client";
|
|
2
|
+
export type StateReducer<State> = (state: State) => State;
|
|
3
|
+
export type StateFollower<State> = (reduce: (reducer: StateReducer<State>) => void) => Cleanup;
|
|
4
|
+
/** One mounted React snapshot composed from an explicit read and future events. */
|
|
5
|
+
export default class LiveState<State> {
|
|
6
|
+
private readonly read;
|
|
7
|
+
private readonly follow;
|
|
8
|
+
private readonly listeners;
|
|
9
|
+
private value;
|
|
10
|
+
private error;
|
|
11
|
+
private stopSource;
|
|
12
|
+
private pending;
|
|
13
|
+
private generation;
|
|
14
|
+
private active;
|
|
15
|
+
constructor(read: () => Promise<State>, follow: StateFollower<State>);
|
|
16
|
+
readonly snapshot: () => State | undefined;
|
|
17
|
+
readonly subscribe: (listener: () => void) => Cleanup;
|
|
18
|
+
private start;
|
|
19
|
+
private stop;
|
|
20
|
+
private fail;
|
|
21
|
+
private update;
|
|
22
|
+
private notify;
|
|
23
|
+
}
|
|
24
|
+
export declare function combineCleanups(...cleanups: Cleanup[]): Cleanup;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const noError = Symbol("no-error");
|
|
2
|
+
/** One mounted React snapshot composed from an explicit read and future events. */
|
|
3
|
+
export default class LiveState {
|
|
4
|
+
read;
|
|
5
|
+
follow;
|
|
6
|
+
listeners = new Set();
|
|
7
|
+
value;
|
|
8
|
+
error = noError;
|
|
9
|
+
stopSource = null;
|
|
10
|
+
pending = [];
|
|
11
|
+
generation = 0;
|
|
12
|
+
active = false;
|
|
13
|
+
constructor(read, follow) {
|
|
14
|
+
this.read = read;
|
|
15
|
+
this.follow = follow;
|
|
16
|
+
}
|
|
17
|
+
snapshot = () => {
|
|
18
|
+
if (this.error !== noError)
|
|
19
|
+
throw this.error;
|
|
20
|
+
return this.value;
|
|
21
|
+
};
|
|
22
|
+
subscribe = (listener) => {
|
|
23
|
+
this.listeners.add(listener);
|
|
24
|
+
if (!this.active)
|
|
25
|
+
this.start();
|
|
26
|
+
return () => {
|
|
27
|
+
this.listeners.delete(listener);
|
|
28
|
+
if (this.listeners.size === 0)
|
|
29
|
+
this.stop();
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
start() {
|
|
33
|
+
this.active = true;
|
|
34
|
+
this.value = undefined;
|
|
35
|
+
this.error = noError;
|
|
36
|
+
this.pending = [];
|
|
37
|
+
const generation = ++this.generation;
|
|
38
|
+
try {
|
|
39
|
+
this.stopSource = this.follow(reducer => {
|
|
40
|
+
if (this.generation !== generation)
|
|
41
|
+
return;
|
|
42
|
+
if (this.value === undefined)
|
|
43
|
+
this.pending.push(reducer);
|
|
44
|
+
else
|
|
45
|
+
this.update(reducer(this.value));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.fail(generation, error);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
void this.read().then(value => {
|
|
53
|
+
if (this.generation !== generation)
|
|
54
|
+
return;
|
|
55
|
+
for (const reducer of this.pending)
|
|
56
|
+
value = reducer(value);
|
|
57
|
+
this.pending = [];
|
|
58
|
+
this.update(value);
|
|
59
|
+
}, error => this.fail(generation, error));
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
this.active = false;
|
|
63
|
+
this.generation++;
|
|
64
|
+
this.stopSource?.();
|
|
65
|
+
this.stopSource = null;
|
|
66
|
+
this.pending = [];
|
|
67
|
+
this.value = undefined;
|
|
68
|
+
this.error = noError;
|
|
69
|
+
}
|
|
70
|
+
fail(generation, error) {
|
|
71
|
+
if (this.generation !== generation)
|
|
72
|
+
return;
|
|
73
|
+
this.stopSource?.();
|
|
74
|
+
this.stopSource = null;
|
|
75
|
+
this.pending = [];
|
|
76
|
+
this.error = error;
|
|
77
|
+
this.notify();
|
|
78
|
+
}
|
|
79
|
+
update(value) {
|
|
80
|
+
if (Object.is(this.value, value))
|
|
81
|
+
return;
|
|
82
|
+
this.value = value;
|
|
83
|
+
this.notify();
|
|
84
|
+
}
|
|
85
|
+
notify() {
|
|
86
|
+
for (const listener of this.listeners)
|
|
87
|
+
listener();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export function combineCleanups(...cleanups) {
|
|
91
|
+
return () => {
|
|
92
|
+
for (const cleanup of cleanups)
|
|
93
|
+
cleanup();
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/main.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export { default as CurrentProvider, useParent, useProcess, useProgram,
|
|
1
|
+
export { default as CurrentProvider, useParent, useProcess, useProgram, type CurrentProvision, type CurrentProvisionName, type CurrentProviderProperties } from "./current-provider.js";
|
|
2
|
+
export { default as HostProvider, usePointerPosition, useSurfaceSize, useHostTheme, type HostProvision, type HostProvisionName, type HostProviderProperties } from "./host-provider.js";
|
|
2
3
|
export { default as useSubscribe } from "./use-subscribe.js";
|
|
3
4
|
export { default as useObserve } from "./use-observe.js";
|
|
5
|
+
export { default as useScale } from "./use-scale.js";
|
|
6
|
+
export { default as useColor } from "./use-color.js";
|
|
7
|
+
export { default as useProgramState, type ProgramState } from "./use-program-state.js";
|
|
8
|
+
export { default as useProcessState, type ProcessState } from "./use-process-state.js";
|
|
9
|
+
export { default as useWindowState } from "./use-window-state.js";
|
|
4
10
|
export { default as useObserveAsks, type AskObservable } from "./use-observe-asks.js";
|
|
5
11
|
export { default as useObserveAnswers, type AnswerObservable } from "./use-observe-answers.js";
|
package/dist/main.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
export { default as CurrentProvider, useParent, useProcess, useProgram
|
|
1
|
+
export { default as CurrentProvider, useParent, useProcess, useProgram } from "./current-provider.js";
|
|
2
|
+
export { default as HostProvider, usePointerPosition, useSurfaceSize, useHostTheme } from "./host-provider.js";
|
|
2
3
|
export { default as useSubscribe } from "./use-subscribe.js";
|
|
3
4
|
export { default as useObserve } from "./use-observe.js";
|
|
5
|
+
export { default as useScale } from "./use-scale.js";
|
|
6
|
+
export { default as useColor } from "./use-color.js";
|
|
7
|
+
export { default as useProgramState } from "./use-program-state.js";
|
|
8
|
+
export { default as useProcessState } from "./use-process-state.js";
|
|
9
|
+
export { default as useWindowState } from "./use-window-state.js";
|
|
4
10
|
export { default as useObserveAsks } from "./use-observe-asks.js";
|
|
5
11
|
export { default as useObserveAnswers } from "./use-observe-answers.js";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AnswerCapture, AnswerObserver, Cleanup } from "@phreshos/client";
|
|
2
2
|
/** Any Server traffic surface capable of observing its outgoing answers. */
|
|
3
3
|
export type AnswerObservable = Readonly<{
|
|
4
|
+
/** Observes answers sent by the target Server and returns its cleanup. */
|
|
4
5
|
observeAnswers<Result = unknown>(observer: AnswerObserver<Result>): Cleanup;
|
|
5
6
|
}>;
|
|
6
7
|
/**
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AskCapture, AskObserver, Cleanup } from "@phreshos/client";
|
|
2
2
|
/** Any Endpoint traffic surface capable of observing its outgoing questions. */
|
|
3
3
|
export type AskObservable = Readonly<{
|
|
4
|
+
/** Observes questions sent by the target Endpoint and returns its cleanup. */
|
|
4
5
|
observeAsks<Payload = unknown>(observer: AskObserver<Payload>): Cleanup;
|
|
5
6
|
}>;
|
|
6
7
|
/**
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Process } from "@phreshos/client";
|
|
2
|
+
/** Mutable lifecycle state of one Process and its Endpoint incarnations. */
|
|
3
|
+
export type ProcessState = Readonly<{
|
|
4
|
+
exited: boolean;
|
|
5
|
+
serverExists: boolean;
|
|
6
|
+
clientExists: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
/** Explicitly reads and follows one Process while this hook is mounted. */
|
|
9
|
+
export default function useProcessState(process: Process): ProcessState | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import LiveState, { combineCleanups } from "./live-state.js";
|
|
3
|
+
/** Explicitly reads and follows one Process while this hook is mounted. */
|
|
4
|
+
export default function useProcessState(process) {
|
|
5
|
+
const state = useMemo(() => new LiveState(async () => {
|
|
6
|
+
const [exited, serverExists, clientExists] = await Promise.all([
|
|
7
|
+
process.exited(),
|
|
8
|
+
process.server.exists(),
|
|
9
|
+
process.client.exists()
|
|
10
|
+
]);
|
|
11
|
+
return { exited, serverExists, clientExists };
|
|
12
|
+
}, reduce => combineCleanups(process.subscribe("endpointStart", endpoint => reduce(current => endpointPresence(current, process, endpoint, true))), process.subscribe("endpointStop", endpoint => reduce(current => endpointPresence(current, process, endpoint, false))), process.subscribe("exit", () => reduce(current => ({ ...current, exited: true, serverExists: false, clientExists: false }))))), [process]);
|
|
13
|
+
return useSyncExternalStore(state.subscribe, state.snapshot, state.snapshot);
|
|
14
|
+
}
|
|
15
|
+
function endpointPresence(state, process, endpoint, exists) {
|
|
16
|
+
if (endpoint === process.server) {
|
|
17
|
+
return state.serverExists === exists ? state : { ...state, serverExists: exists };
|
|
18
|
+
}
|
|
19
|
+
if (endpoint === process.client) {
|
|
20
|
+
return state.clientExists === exists ? state : { ...state, clientExists: exists };
|
|
21
|
+
}
|
|
22
|
+
return state;
|
|
23
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Process, Program } from "@phreshos/client";
|
|
2
|
+
/** Mutable runtime state derived from one Program's reads and live events. */
|
|
3
|
+
export type ProgramState = Readonly<{
|
|
4
|
+
installed: boolean;
|
|
5
|
+
processes: readonly Process[];
|
|
6
|
+
}>;
|
|
7
|
+
/** Explicitly reads and follows one Program while this hook is mounted. */
|
|
8
|
+
export default function useProgramState(program: Program): ProgramState | undefined;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import LiveState, { combineCleanups } from "./live-state.js";
|
|
3
|
+
/** Explicitly reads and follows one Program while this hook is mounted. */
|
|
4
|
+
export default function useProgramState(program) {
|
|
5
|
+
const state = useMemo(() => new LiveState(async () => {
|
|
6
|
+
const [installed, processes] = await Promise.all([
|
|
7
|
+
program.installed(),
|
|
8
|
+
program.processes()
|
|
9
|
+
]);
|
|
10
|
+
return { installed, processes };
|
|
11
|
+
}, reduce => combineCleanups(program.subscribe("processCreate", process => reduce(current => addProcess(current, process))), program.subscribe("processExit", ({ process }) => reduce(current => removeProcess(current, process))), program.subscribe("uninstall", () => reduce(current => current.installed ? { ...current, installed: false } : current)))), [program]);
|
|
12
|
+
return useSyncExternalStore(state.subscribe, state.snapshot, state.snapshot);
|
|
13
|
+
}
|
|
14
|
+
function addProcess(state, process) {
|
|
15
|
+
if (state.processes.includes(process))
|
|
16
|
+
return state;
|
|
17
|
+
return { ...state, processes: [...state.processes, process] };
|
|
18
|
+
}
|
|
19
|
+
function removeProcess(state, process) {
|
|
20
|
+
const processes = state.processes.filter(candidate => candidate !== process);
|
|
21
|
+
return processes.length === state.processes.length ? state : { ...state, processes };
|
|
22
|
+
}
|
package/dist/use-subscribe.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EventMessage, EventName, Subscribable } from "@phreshos/client";
|
|
1
|
+
import type { Channel, ChannelMessage, Endpoint, EventMessage, EventName, Subscribable, SubscribableEvents, SubscribableFallback } from "@phreshos/client";
|
|
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> = {
|
|
@@ -6,12 +6,22 @@ type CompatibleEvent<Events extends object, Fallback, Narrowed> = {
|
|
|
6
6
|
}[EventName<Events>] | ([
|
|
7
7
|
Fallback
|
|
8
8
|
] extends [never] ? never : Narrowed extends Fallback ? OpenEvent : never);
|
|
9
|
+
type SubscribableTarget = Readonly<{
|
|
10
|
+
subscribe: unknown;
|
|
11
|
+
}>;
|
|
12
|
+
type TargetEvent<Target> = AvailableEvent<SubscribableEvents<Target>, SubscribableFallback<Target>>;
|
|
13
|
+
type TargetMessage<Target, Event extends string> = EventMessage<SubscribableEvents<Target>, SubscribableFallback<Target>, Event>;
|
|
9
14
|
/**
|
|
10
|
-
* Subscribes while mounted and retains the latest projected result.
|
|
15
|
+
* Subscribes while mounted and retains the latest message or projected result.
|
|
11
16
|
*
|
|
12
17
|
* The target supplies known message types. An explicit generic or callback
|
|
13
18
|
* annotation may narrow that type, but cannot replace it incompatibly.
|
|
14
19
|
*/
|
|
20
|
+
export default function useSubscribe<Target extends SubscribableTarget, Event extends TargetEvent<Target>>(target: Target, event: Event): TargetMessage<Target, Event> | undefined;
|
|
21
|
+
export default function useSubscribe<Narrowed = unknown>(target: Endpoint, event: string): Narrowed | undefined;
|
|
22
|
+
export default function useSubscribe<Narrowed extends ChannelMessage<unknown> = ChannelMessage<unknown>>(target: Channel, event: string): Narrowed | undefined;
|
|
23
|
+
export default function useSubscribe<Target extends SubscribableTarget, Event extends TargetEvent<Target>, Result>(target: Target, event: Event, project: (message: TargetMessage<Target, Event>) => Result): Awaited<Result> | undefined;
|
|
24
|
+
export default function useSubscribe<Narrowed, Result = unknown>(target: Endpoint, event: string, project: (message: Narrowed) => Result): Awaited<Result> | undefined;
|
|
25
|
+
export default function useSubscribe<Narrowed extends ChannelMessage<unknown>, Result = unknown>(target: Channel, event: string, project: (message: Narrowed) => Result): Awaited<Result> | undefined;
|
|
15
26
|
export default function useSubscribe<Narrowed, Events extends object, Fallback, Result>(target: Subscribable<Events, Fallback>, event: CompatibleEvent<Events, Fallback, Narrowed>, project: (message: Narrowed) => Result): Awaited<Result> | undefined;
|
|
16
|
-
export default function useSubscribe<Events extends object, Fallback, Event extends AvailableEvent<Events, Fallback>, Result>(target: Subscribable<Events, Fallback>, event: Event, project: (message: EventMessage<Events, Fallback, Event>) => Result): Awaited<Result> | undefined;
|
|
17
27
|
export {};
|
package/dist/use-subscribe.js
CHANGED
|
@@ -5,5 +5,8 @@ export default function useSubscribe(target, event, project) {
|
|
|
5
5
|
const subscribe = target.subscribe;
|
|
6
6
|
return subscribe(event, receive);
|
|
7
7
|
}, [target, event]);
|
|
8
|
-
return useEventResult(connect, project);
|
|
8
|
+
return useEventResult(connect, project ?? identity);
|
|
9
|
+
}
|
|
10
|
+
function identity(message) {
|
|
11
|
+
return message;
|
|
9
12
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useMemo, useSyncExternalStore } from "react";
|
|
2
|
+
import LiveState, { combineCleanups } from "./live-state.js";
|
|
3
|
+
/** Explicitly reads and follows one live Client Window while mounted. */
|
|
4
|
+
export default function useWindowState(window) {
|
|
5
|
+
const state = useMemo(() => new LiveState(async () => {
|
|
6
|
+
const [title, position, size, minimized, front, layer, location] = await Promise.all([
|
|
7
|
+
window.title(),
|
|
8
|
+
window.position(),
|
|
9
|
+
window.size(),
|
|
10
|
+
window.minimized(),
|
|
11
|
+
window.front(),
|
|
12
|
+
window.layer(),
|
|
13
|
+
window.location()
|
|
14
|
+
]);
|
|
15
|
+
return { title, position, size, minimized, front, layer, location };
|
|
16
|
+
}, reduce => combineCleanups(window.subscribe("move", position => reduce(current => ({ ...current, position }))), window.subscribe("resize", size => reduce(current => ({ ...current, size }))), window.subscribe("minimize", minimized => reduce(current => ({ ...current, minimized }))), window.subscribe("changeTitle", title => reduce(current => ({ ...current, title }))), window.subscribe("front", front => reduce(current => ({ ...current, front }))))), [window]);
|
|
17
|
+
return useSyncExternalStore(state.subscribe, state.snapshot, state.snapshot);
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phreshos/react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "React adapters for the Client SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|
|
@@ -13,21 +13,54 @@
|
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"dist",
|
|
16
|
+
"LICENSE",
|
|
16
17
|
"README.md"
|
|
17
18
|
],
|
|
19
|
+
"author": "Zohayr SLILEH",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/PhreshOS/react.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/PhreshOS/react/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/PhreshOS/react#readme",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"phreshos",
|
|
31
|
+
"react",
|
|
32
|
+
"sdk",
|
|
33
|
+
"typescript"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public",
|
|
37
|
+
"provenance": true
|
|
38
|
+
},
|
|
39
|
+
"packageManager": "bun@1.3.14",
|
|
18
40
|
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
41
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
42
|
+
"check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
43
|
+
"test": "vitest run --environment jsdom",
|
|
44
|
+
"build": "node --run clean && tsc --noEmit false --outDir dist --rootDir source",
|
|
45
|
+
"verify:package": "node scripts/verify-package.mjs",
|
|
46
|
+
"verify": "node --run check && node --run test && node --run build && node --run verify:package",
|
|
47
|
+
"prepack": "node --run test && node --run build"
|
|
22
48
|
},
|
|
23
49
|
"peerDependencies": {
|
|
24
|
-
"@phreshos/client": "^0.1.
|
|
50
|
+
"@phreshos/client": "^0.1.2",
|
|
51
|
+
"@phreshos/core": "^0.1.2",
|
|
25
52
|
"react": "^19.2.0"
|
|
26
53
|
},
|
|
27
54
|
"devDependencies": {
|
|
28
|
-
"@phreshos/client": "0.1.
|
|
55
|
+
"@phreshos/client": "^0.1.2",
|
|
56
|
+
"@phreshos/core": "^0.1.2",
|
|
57
|
+
"@testing-library/react": "^16.3.0",
|
|
29
58
|
"@types/react": "^19.2.18",
|
|
59
|
+
"@types/react-dom": "^19.2.4",
|
|
60
|
+
"jsdom": "^30.0.1",
|
|
30
61
|
"react": "^19.2.8",
|
|
31
|
-
"
|
|
62
|
+
"react-dom": "^19.2.8",
|
|
63
|
+
"typescript": "^6.0.3",
|
|
64
|
+
"vitest": "^4.1.1"
|
|
32
65
|
}
|
|
33
66
|
}
|