@phreshos/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # `@phreshos/react`
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
+ - `CurrentProvider` resolves the Client SDK's current Program, Process, parent,
20
+ and Window, then exposes them synchronously through `useProgram()`,
21
+ `useProcess()`, `useParent()`, and `useWindow()`.
22
+ - `useSubscribe()` and `useObserve()` own persistent ordinary-event
23
+ registrations for one mounted React consumer.
24
+ - `useObserveAsks()` adapts an Endpoint traffic surface's question observation,
25
+ while `useObserveAnswers()` adapts a Server traffic surface's answer
26
+ observation.
27
+
28
+ All four hooks retain the latest value returned by their projector.
29
+
30
+ ```tsx
31
+ import { current } from "@phreshos/client"
32
+ import {
33
+ CurrentProvider,
34
+ useProgram,
35
+ useSubscribe
36
+ } from "@phreshos/react"
37
+
38
+ function Counter() {
39
+ const program = useProgram()
40
+
41
+ const count = useSubscribe(current, "count", message => {
42
+ return Number(message.payload)
43
+ })
44
+
45
+ return <p>{program.name}: {count ?? 0}</p>
46
+ }
47
+
48
+ export default function App() {
49
+ return (
50
+ <CurrentProvider fallback={<p>Loading…</p>} waitServer>
51
+ <Counter />
52
+ </CurrentProvider>
53
+ )
54
+ }
55
+ ```
56
+
57
+ Each hook calls the registration's returned cleanup when the component no
58
+ longer consumes it. Projectors may return a value or Promise; only the latest
59
+ invocation may update the hook state. Projector failures are never converted
60
+ into communication, logged, or suppressed by the SDK; they remain local to the
61
+ React application.
62
+
63
+ React is a peer dependency, so this package never installs or bundles a second
64
+ copy into an application.
@@ -0,0 +1,34 @@
1
+ import { type ReactNode } from "react";
2
+ import { type Process, type Program, type Window } from "@phreshos/client";
3
+ /**
4
+ * Resolves the current domain handles before rendering its children.
5
+ *
6
+ * The provider owns no domain state. It only turns asynchronous Client SDK
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}. */
16
+ export declare function useProgram(): Program;
17
+ /** Returns the current Process from the nearest {@link CurrentProvider}. */
18
+ export declare function useProcess(): Process;
19
+ /** Returns the current Process's visible parent from the nearest provider. */
20
+ export declare function useParent(): Process | null;
21
+ /** Returns the current Window from the nearest {@link CurrentProvider}. */
22
+ export declare function useWindow(): Window;
23
+ /** Properties accepted by {@link CurrentProvider}. */
24
+ export interface CurrentProviderProperties {
25
+ /** Content rendered after every current handle has resolved. */
26
+ readonly children: ReactNode;
27
+ /** Content rendered while current handles or optional Server readiness resolve. */
28
+ readonly fallback: ReactNode;
29
+ /**
30
+ * Waits for the Process's Server before rendering children.
31
+ * `true` uses the default ten-second deadline; a number sets milliseconds.
32
+ */
33
+ readonly waitServer?: boolean | number;
34
+ }
@@ -0,0 +1,64 @@
1
+ import { createContext, createElement, useContext, useEffect, useState } from "react";
2
+ import { current } from "@phreshos/client";
3
+ const CurrentContext = createContext(null);
4
+ /**
5
+ * Resolves the current domain handles before rendering its children.
6
+ *
7
+ * The provider owns no domain state. It only turns asynchronous Client SDK
8
+ * navigation into synchronous React context for the hooks below it.
9
+ */
10
+ export default function CurrentProvider({ children, fallback, waitServer = false }) {
11
+ const [resolution, setResolution] = useState({ status: "pending" });
12
+ useEffect(() => {
13
+ let active = true;
14
+ setResolution({ status: "pending" });
15
+ void resolveCurrent(waitServer).then(value => {
16
+ if (active)
17
+ setResolution({ status: "ready", value });
18
+ }, error => {
19
+ if (active)
20
+ setResolution({ status: "error", error });
21
+ });
22
+ return () => { active = false; };
23
+ }, [waitServer]);
24
+ if (resolution.status === "pending")
25
+ return fallback;
26
+ if (resolution.status === "error")
27
+ throw resolution.error;
28
+ return createElement(CurrentContext.Provider, { value: resolution.value }, children);
29
+ }
30
+ async function resolveCurrent(waitServer) {
31
+ const serverReady = typeof waitServer === "number"
32
+ ? current.server.waitReady(waitServer)
33
+ : waitServer ? current.server.waitReady() : undefined;
34
+ const [program, process, parent] = await Promise.all([
35
+ current.program(),
36
+ current.process(),
37
+ current.parent(),
38
+ serverReady
39
+ ]);
40
+ const window = await current.window();
41
+ return { program, process, parent, window };
42
+ }
43
+ /** Returns the current Program from the nearest {@link CurrentProvider}. */
44
+ export function useProgram() {
45
+ return useCurrent().program;
46
+ }
47
+ /** Returns the current Process from the nearest {@link CurrentProvider}. */
48
+ export function useProcess() {
49
+ return useCurrent().process;
50
+ }
51
+ /** Returns the current Process's visible parent from the nearest provider. */
52
+ export function useParent() {
53
+ return useCurrent().parent;
54
+ }
55
+ /** Returns the current Window from the nearest {@link CurrentProvider}. */
56
+ export function useWindow() {
57
+ return useCurrent().window;
58
+ }
59
+ function useCurrent() {
60
+ const value = useContext(CurrentContext);
61
+ if (!value)
62
+ throw new Error("Current hooks must be used inside CurrentProvider");
63
+ return value;
64
+ }
@@ -0,0 +1,19 @@
1
+ import type { Cleanup } from "@phreshos/client";
2
+ /** A persistent SDK registration consumed by the React adapter. */
3
+ export type Connect<Message> = (receive: (message: Message) => void) => Cleanup;
4
+ /** Retains only the latest projected event value for one mounted hook. */
5
+ export default function useEventResult<Message, Result>(connect: Connect<Message>, project: (message: Message) => Result): Awaited<Result> | undefined;
6
+ /** Bridges one persistent SDK registration into a React external store. */
7
+ export declare class EventResult<Message> {
8
+ project: (message: Message) => unknown;
9
+ private readonly connect;
10
+ private readonly listeners;
11
+ private value;
12
+ private stop;
13
+ private invocation;
14
+ constructor(connect: Connect<Message>);
15
+ readonly snapshot: () => unknown;
16
+ readonly subscribe: (listener: () => void) => () => void;
17
+ private readonly receive;
18
+ private update;
19
+ }
@@ -0,0 +1,58 @@
1
+ import { useEffectEvent, useMemo, useSyncExternalStore } from "react";
2
+ /** Retains only the latest projected event value for one mounted hook. */
3
+ export default function useEventResult(connect, project) {
4
+ const latestProject = useEffectEvent(project);
5
+ const state = useMemo(() => new EventResult(connect), [connect]);
6
+ state.project = latestProject;
7
+ return useSyncExternalStore(state.subscribe, state.snapshot, state.snapshot);
8
+ }
9
+ /** Bridges one persistent SDK registration into a React external store. */
10
+ export class EventResult {
11
+ project = () => undefined;
12
+ connect;
13
+ listeners = new Set();
14
+ value;
15
+ stop = null;
16
+ invocation = 0;
17
+ constructor(connect) {
18
+ this.connect = connect;
19
+ }
20
+ snapshot = () => this.value;
21
+ subscribe = (listener) => {
22
+ this.listeners.add(listener);
23
+ if (!this.stop)
24
+ this.stop = this.connect(this.receive);
25
+ return () => {
26
+ this.listeners.delete(listener);
27
+ if (this.listeners.size > 0)
28
+ return;
29
+ this.invocation++;
30
+ this.stop?.();
31
+ this.stop = null;
32
+ };
33
+ };
34
+ receive = (message) => {
35
+ const invocation = ++this.invocation;
36
+ const result = this.project(message);
37
+ if (isPromiseLike(result)) {
38
+ void Promise.resolve(result).then(value => {
39
+ if (invocation === this.invocation)
40
+ this.update(value);
41
+ });
42
+ }
43
+ else
44
+ this.update(result);
45
+ };
46
+ update(value) {
47
+ if (Object.is(this.value, value))
48
+ return;
49
+ this.value = value;
50
+ for (const listener of this.listeners)
51
+ listener();
52
+ }
53
+ }
54
+ function isPromiseLike(value) {
55
+ return (typeof value === "object" && value !== null || typeof value === "function")
56
+ && "then" in value
57
+ && typeof value.then === "function";
58
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { default as CurrentProvider, useParent, useProcess, useProgram, useWindow, type CurrentProviderProperties } from "./current-provider.js";
2
+ export { default as useSubscribe } from "./use-subscribe.js";
3
+ export { default as useObserve } from "./use-observe.js";
4
+ export { default as useObserveAsks, type AskObservable } from "./use-observe-asks.js";
5
+ export { default as useObserveAnswers, type AnswerObservable } from "./use-observe-answers.js";
package/dist/main.js ADDED
@@ -0,0 +1,5 @@
1
+ export { default as CurrentProvider, useParent, useProcess, useProgram, useWindow } from "./current-provider.js";
2
+ export { default as useSubscribe } from "./use-subscribe.js";
3
+ export { default as useObserve } from "./use-observe.js";
4
+ export { default as useObserveAsks } from "./use-observe-asks.js";
5
+ export { default as useObserveAnswers } from "./use-observe-answers.js";
@@ -0,0 +1,10 @@
1
+ import type { AnswerCapture, AnswerObserver, Cleanup } from "@phreshos/client";
2
+ /** Any Server traffic surface capable of observing its outgoing answers. */
3
+ export type AnswerObservable = Readonly<{
4
+ observeAnswers<Result = unknown>(observer: AnswerObserver<Result>): Cleanup;
5
+ }>;
6
+ /**
7
+ * Observes answers in one Server's traffic while mounted and retains the
8
+ * latest projected result.
9
+ */
10
+ export default function useObserveAnswers<Answer, Result>(target: AnswerObservable, project: (capture: AnswerCapture<Answer>) => Result): Awaited<Result> | undefined;
@@ -0,0 +1,10 @@
1
+ import { useCallback } from "react";
2
+ import useEventResult from "./event-result.js";
3
+ /**
4
+ * Observes answers in one Server's traffic while mounted and retains the
5
+ * latest projected result.
6
+ */
7
+ export default function useObserveAnswers(target, project) {
8
+ const connect = useCallback((receive) => target.observeAnswers(receive), [target]);
9
+ return useEventResult(connect, project);
10
+ }
@@ -0,0 +1,10 @@
1
+ import type { AskCapture, AskObserver, Cleanup } from "@phreshos/client";
2
+ /** Any Endpoint traffic surface capable of observing its outgoing questions. */
3
+ export type AskObservable = Readonly<{
4
+ observeAsks<Payload = unknown>(observer: AskObserver<Payload>): Cleanup;
5
+ }>;
6
+ /**
7
+ * Observes questions in one Endpoint's traffic while mounted and retains the
8
+ * latest projected result.
9
+ */
10
+ export default function useObserveAsks<Payload, Result>(target: AskObservable, project: (capture: AskCapture<Payload>) => Result): Awaited<Result> | undefined;
@@ -0,0 +1,10 @@
1
+ import { useCallback } from "react";
2
+ import useEventResult from "./event-result.js";
3
+ /**
4
+ * Observes questions in one Endpoint's traffic while mounted and retains the
5
+ * latest projected result.
6
+ */
7
+ export default function useObserveAsks(target, project) {
8
+ const connect = useCallback((receive) => target.observeAsks(receive), [target]);
9
+ return useEventResult(connect, project);
10
+ }
@@ -0,0 +1,3 @@
1
+ import type { Captures, Subscribable } from "@phreshos/client";
2
+ /** Observes all events while mounted and retains the latest projected result. */
3
+ export default function useObserve<Events extends object, Fallback, Result>(target: Subscribable<Events, Fallback>, project: (capture: Captures<Events, Fallback>) => Result): Awaited<Result> | undefined;
@@ -0,0 +1,7 @@
1
+ import { useCallback } from "react";
2
+ import useEventResult from "./event-result.js";
3
+ /** Observes all events while mounted and retains the latest projected result. */
4
+ export default function useObserve(target, project) {
5
+ const connect = useCallback((receive) => target.observe(receive), [target]);
6
+ return useEventResult(connect, project);
7
+ }
@@ -0,0 +1,17 @@
1
+ import type { EventMessage, EventName, Subscribable } from "@phreshos/client";
2
+ type OpenEvent = string & {};
3
+ type AvailableEvent<Events extends object, Fallback> = EventName<Events> | ([Fallback] extends [never] ? never : OpenEvent);
4
+ type CompatibleEvent<Events extends object, Fallback, Narrowed> = {
5
+ [Event in EventName<Events>]: Narrowed extends Events[Event] ? Event : never;
6
+ }[EventName<Events>] | ([
7
+ Fallback
8
+ ] extends [never] ? never : Narrowed extends Fallback ? OpenEvent : never);
9
+ /**
10
+ * Subscribes while mounted and retains the latest projected result.
11
+ *
12
+ * The target supplies known message types. An explicit generic or callback
13
+ * annotation may narrow that type, but cannot replace it incompatibly.
14
+ */
15
+ 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
+ export {};
@@ -0,0 +1,9 @@
1
+ import { useCallback } from "react";
2
+ import useEventResult from "./event-result.js";
3
+ export default function useSubscribe(target, event, project) {
4
+ const connect = useCallback((receive) => {
5
+ const subscribe = target.subscribe;
6
+ return subscribe(event, receive);
7
+ }, [target, event]);
8
+ return useEventResult(connect, project);
9
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@phreshos/react",
3
+ "version": "0.1.0",
4
+ "description": "React adapters for the Client SDK.",
5
+ "type": "module",
6
+ "main": "dist/main.js",
7
+ "types": "dist/main.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/main.d.ts",
11
+ "default": "./dist/main.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "check": "tsc --noEmit",
20
+ "build": "tsc --noEmit false --outDir dist --rootDir source",
21
+ "prepack": "node --run build"
22
+ },
23
+ "peerDependencies": {
24
+ "@phreshos/client": "^0.1.0",
25
+ "react": "^19.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "@phreshos/client": "0.1.0",
29
+ "@types/react": "^19.2.18",
30
+ "react": "^19.2.8",
31
+ "typescript": "^6.0.3"
32
+ }
33
+ }