@synapse-sdk/react 0.3.4 → 0.3.7
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 +47 -1
- package/dist/index.d.ts +39 -2
- package/dist/index.js +43 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
React hooks for a Synapse node: `<SynapseProvider node={node}>` above your components, then `useConnection()`,
|
|
4
4
|
`useProject()`, `useCatalog()` (the node's `status`, `project` and `catalog` cells), `useTopic(t, { select })`, `useCall(svc)`, `useServe(svc, handler)`,
|
|
5
5
|
`usePublisher(t)`, `useStore(name)`, `useScene()`, `useSceneEntity(scene, id | { name }, { select })`, `useSceneTree(scene)`,
|
|
6
|
-
`useSceneStatus(scene)
|
|
6
|
+
`useSceneStatus(scene)`, `useTasks(select?)` and `useDiscover()` (your clusters, once signed in). Every hook reads a value the node keeps current; none keeps its own
|
|
7
7
|
copy.
|
|
8
8
|
|
|
9
9
|
## Two kinds of users
|
|
@@ -78,6 +78,52 @@ wins, and a call superseded by a newer `connect()` or `disconnect()` resolves qu
|
|
|
78
78
|
nothing and settles with it. `connect()` with no target settles with the declared one. `failed` is not the end: the
|
|
79
79
|
node rests there until the next `connect()`; only `close()` ends it (`closed`).
|
|
80
80
|
|
|
81
|
+
## Signing in: robots by name
|
|
82
|
+
|
|
83
|
+
A signed-in person can connect to a robot by name, `node.connect("pi4-1@taiga/devel")`, and list their clusters and
|
|
84
|
+
robots, from the browser. The SDK asks the key server for your clusters and for a token to the cluster's relay; it
|
|
85
|
+
needs only a way to get the person's credential. With Clerk, pass Clerk's own `useAuth` to the provider:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useAuth } from "@clerk/clerk-react";
|
|
89
|
+
import { SynapseProvider, useDiscover } from "@synapse-sdk/react";
|
|
90
|
+
|
|
91
|
+
<SynapseProvider node={node} useAuth={useAuth}>…</SynapseProvider>
|
|
92
|
+
|
|
93
|
+
function Robots() {
|
|
94
|
+
const { status, data, error, refresh } = useDiscover();
|
|
95
|
+
if (status === "idle") return <span>sign in to see your robots</span>;
|
|
96
|
+
if (status === "error") return <button onClick={refresh}>{error!.message}: try again</button>;
|
|
97
|
+
if (!data) return <span>loading…</span>;
|
|
98
|
+
return <>{data.clusters.flatMap(c => Object.keys(c.runtimes).map(robot =>
|
|
99
|
+
<button key={`${c.id}/${robot}`} onClick={() => node.connect(`${robot}@${c.label}`)}>{robot} ({c.label})</button>))}</>;
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
- The provider follows the sign-in: signed in → `node.setAuth(...)`, signed out → `node.setAuth(undefined)`, another
|
|
104
|
+
person signed in without a sign-out between (Clerk's `userId` changes) → a new sign-in. Pass `useAuth` on every
|
|
105
|
+
render (it is a hook).
|
|
106
|
+
- `useDiscover()` is `{ status, data, error, refresh }` over `node.discovery`: `idle` with nobody signed in, then
|
|
107
|
+
`pending` → `success` with your clusters (`label` is what `connect()` takes after the `@`). It asks once when a person
|
|
108
|
+
signs in; every component using it shares the one answer. `refresh()` asks again.
|
|
109
|
+
- Signing out never ends a live session. The next time the node has to dial (a dropped connection, or a new
|
|
110
|
+
`connect()`), it fails `unauthorized` and `useConnection()` shows `failed`; sign in again and `connect()`.
|
|
111
|
+
- The node keeps what the key server said (your clusters, a token per cluster for up to 12 hours) in memory only, until
|
|
112
|
+
the next sign-in or sign-out: a reconnect costs no request to the key server.
|
|
113
|
+
- Without React, the same on the node: `node.setAuth(fetchToken)` with a function that gives the person's credential
|
|
114
|
+
when asked (`null` when nobody is signed in), `node.setAuth(undefined)` on sign-out, `await node.discover()`,
|
|
115
|
+
`node.discovery.get()`. A script that signs in with a known credential: `node.setAuth(async () => token)`. The
|
|
116
|
+
option `authUrl` names another key server, for example an `http://` one on your own machine while developing; never
|
|
117
|
+
use an `http://` key server in production, the credential travels in the header.
|
|
118
|
+
|
|
119
|
+
During development, without Clerk: `fakeAuth(token)` is a stand-in that is always signed in with `token`, for example
|
|
120
|
+
the login `synapse login` keeps on your machine (`~/.synapse/session`), given through an environment variable and never
|
|
121
|
+
committed:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
<SynapseProvider node={node} useAuth={fakeAuth(import.meta.env.VITE_SYNAPSE_TOKEN)}>…</SynapseProvider>
|
|
125
|
+
```
|
|
126
|
+
|
|
81
127
|
## Calls
|
|
82
128
|
|
|
83
129
|
`useCall(svc, opts)` is the one call hook, request/reply or streaming: `{ call, cancel, status, data, error, updates,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { Synapse, Catalog, ConnectionState, ViewOptions, ViewState, Run, CallOptions, Publisher, ServeHandler, ServeOptions, Registration, StoreHandle, StoreOptions, SceneEntity, SceneEntityRef, SceneHandle, SceneStatus, SceneTree, TasksStatus, TaskCatalogEntry, TasksRun } from "@synapse-sdk/client";
|
|
2
|
+
import type { Synapse, Catalog, ConnectionState, Discovery, ViewOptions, ViewState, Run, CallOptions, Publisher, ServeHandler, ServeOptions, Registration, StoreHandle, StoreOptions, SceneEntity, SceneEntityRef, SceneHandle, SceneStatus, SceneTree, TasksStatus, TaskCatalogEntry, TasksRun } from "@synapse-sdk/client";
|
|
3
3
|
import type { ErrorBody, ProjectSnapshot, ServiceRef, TopicRef, SynapseError } from "@synapse-sdk/core";
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* What `useAuth` gives (the shape of Clerk's `useAuth()`): whether the sign-in state is known yet, whether a person is
|
|
6
|
+
* signed in, who (`userId`, when the hook has it: another person signing in without a sign-out between is a new
|
|
7
|
+
* sign-in to the node), and their credential for the key server, fetched when asked.
|
|
8
|
+
*/
|
|
9
|
+
type AuthState = {
|
|
10
|
+
isLoaded: boolean;
|
|
11
|
+
isSignedIn: boolean | undefined;
|
|
12
|
+
userId?: string | null;
|
|
13
|
+
getToken(): Promise<string | null>;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Makes `node` the node of every hook below. With `useAuth` (Clerk's own `useAuth`, or `fakeAuth(token)`), the person
|
|
17
|
+
* signed in there is the node's (`node.setAuth`): signed in → the node asks the key server with their credential;
|
|
18
|
+
* signed out → `node.setAuth(undefined)`; not known yet → nothing; a different `userId` while signed in → a new sign-in. `useAuth` is a hook: pass it on every render or never.
|
|
19
|
+
* Signing out ends no live session.
|
|
20
|
+
*/
|
|
21
|
+
export declare function SynapseProvider({ node, useAuth, children }: {
|
|
5
22
|
node: Synapse;
|
|
23
|
+
useAuth?: () => AuthState;
|
|
6
24
|
children?: ReactNode;
|
|
7
25
|
}): import("react").FunctionComponentElement<import("react").ProviderProps<Synapse | null>>;
|
|
26
|
+
/**
|
|
27
|
+
* A stand-in for Clerk's `useAuth` that is always signed in with `token` (a key-server credential such as the one
|
|
28
|
+
* `synapse login` keeps on your machine), for development and tests: `<SynapseProvider node={node} useAuth={fakeAuth(token)}>`.
|
|
29
|
+
* An empty `token` is signed out. Keep the token out of your source code (an environment variable).
|
|
30
|
+
*/
|
|
31
|
+
export declare function fakeAuth(token: string): () => AuthState;
|
|
8
32
|
export declare function useSynapse(): Synapse;
|
|
9
33
|
/** The node from the nearest `<SynapseProvider>`, or `null` when there is none. */
|
|
10
34
|
export declare function useSynapseOptional(): Synapse | null;
|
|
@@ -14,6 +38,18 @@ export declare function useConnection(): ConnectionState;
|
|
|
14
38
|
export declare function useProject(): ProjectSnapshot | undefined;
|
|
15
39
|
/** The runtime's catalogs as the current session last read them (`node.catalog`). */
|
|
16
40
|
export declare function useCatalog(): Catalog;
|
|
41
|
+
/**
|
|
42
|
+
* The signed-in person's clusters and their runtimes (`node.discovery`): `status` (`idle` with nobody signed in, `pending`,
|
|
43
|
+
* `success`, `error`), `data` (the key server's answer; still the last one on `error`), `error`, and `refresh()` to ask
|
|
44
|
+
* again. Asks the key server once when it shows `idle` and a person is signed in; every component using it shares one
|
|
45
|
+
* value and one request.
|
|
46
|
+
*/
|
|
47
|
+
export declare function useDiscover(): {
|
|
48
|
+
status: "idle" | "pending" | "success" | "error";
|
|
49
|
+
data: Discovery | undefined;
|
|
50
|
+
error: SynapseError | undefined;
|
|
51
|
+
refresh: () => void;
|
|
52
|
+
};
|
|
17
53
|
/** The last `limit` notices (`node.notices`) since the component mounted, newest last. */
|
|
18
54
|
export declare function useNotices(limit?: number): ErrorBody[];
|
|
19
55
|
/** Typed disabled sentinel: `useTopic("skip")` renders `pending` without subscribing. */
|
|
@@ -116,3 +152,4 @@ export interface TasksView {
|
|
|
116
152
|
*/
|
|
117
153
|
export declare function useTasks(): TasksView;
|
|
118
154
|
export declare function useTasks<S>(select: (t: TasksView) => S): S;
|
|
155
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -7,9 +7,36 @@ import { createContext, createElement, useCallback, useContext, useEffect, useMe
|
|
|
7
7
|
import { replaceEqualDeep, shallowEqual } from "@synapse-sdk/client";
|
|
8
8
|
import { toSynapseError } from "@synapse-sdk/core";
|
|
9
9
|
const SynapseContext = createContext(null);
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Makes `node` the node of every hook below. With `useAuth` (Clerk's own `useAuth`, or `fakeAuth(token)`), the person
|
|
12
|
+
* signed in there is the node's (`node.setAuth`): signed in → the node asks the key server with their credential;
|
|
13
|
+
* signed out → `node.setAuth(undefined)`; not known yet → nothing; a different `userId` while signed in → a new sign-in. `useAuth` is a hook: pass it on every render or never.
|
|
14
|
+
* Signing out ends no live session.
|
|
15
|
+
*/
|
|
16
|
+
export function SynapseProvider({ node, useAuth, children }) {
|
|
17
|
+
const auth = useAuth?.();
|
|
18
|
+
const getToken = useRef(auth?.getToken);
|
|
19
|
+
getToken.current = auth?.getToken;
|
|
20
|
+
const loaded = auth?.isLoaded, signedIn = auth?.isSignedIn, userId = auth?.userId ?? undefined;
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (!loaded)
|
|
23
|
+
return;
|
|
24
|
+
if (signedIn)
|
|
25
|
+
node.setAuth(() => getToken.current ? getToken.current() : Promise.resolve(null));
|
|
26
|
+
else
|
|
27
|
+
node.setAuth(undefined);
|
|
28
|
+
}, [node, loaded, signedIn, userId]);
|
|
11
29
|
return createElement(SynapseContext.Provider, { value: node }, children);
|
|
12
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* A stand-in for Clerk's `useAuth` that is always signed in with `token` (a key-server credential such as the one
|
|
33
|
+
* `synapse login` keeps on your machine), for development and tests: `<SynapseProvider node={node} useAuth={fakeAuth(token)}>`.
|
|
34
|
+
* An empty `token` is signed out. Keep the token out of your source code (an environment variable).
|
|
35
|
+
*/
|
|
36
|
+
export function fakeAuth(token) {
|
|
37
|
+
const state = token ? { isLoaded: true, isSignedIn: true, getToken: async () => token } : { isLoaded: true, isSignedIn: false, getToken: async () => null };
|
|
38
|
+
return () => state;
|
|
39
|
+
}
|
|
13
40
|
export function useSynapse() {
|
|
14
41
|
const node = useContext(SynapseContext);
|
|
15
42
|
if (!node)
|
|
@@ -33,6 +60,21 @@ export function useCatalog() {
|
|
|
33
60
|
const node = useSynapse();
|
|
34
61
|
return useSyncExternalStore(useCallback((cb) => node.catalog.onChange(() => cb()), [node]), () => node.catalog.get(), () => node.catalog.get());
|
|
35
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* The signed-in person's clusters and their runtimes (`node.discovery`): `status` (`idle` with nobody signed in, `pending`,
|
|
65
|
+
* `success`, `error`), `data` (the key server's answer; still the last one on `error`), `error`, and `refresh()` to ask
|
|
66
|
+
* again. Asks the key server once when it shows `idle` and a person is signed in; every component using it shares one
|
|
67
|
+
* value and one request.
|
|
68
|
+
*/
|
|
69
|
+
export function useDiscover() {
|
|
70
|
+
const node = useSynapse();
|
|
71
|
+
const state = useSyncExternalStore(useCallback((cb) => node.discovery.onChange(() => cb()), [node]), () => node.discovery.get(), () => node.discovery.get());
|
|
72
|
+
// `idle` again after every `setAuth`: asked then (nobody signed in: refused, and it stays `idle`).
|
|
73
|
+
useEffect(() => { if (state.status === "idle")
|
|
74
|
+
node.discover().catch(() => { }); }, [node, state]);
|
|
75
|
+
const refresh = useCallback(() => { node.discover().catch(() => { }); }, [node]);
|
|
76
|
+
return { status: state.status, data: state.data, error: state.error, refresh };
|
|
77
|
+
}
|
|
36
78
|
/** The last `limit` notices (`node.notices`) since the component mounted, newest last. */
|
|
37
79
|
export function useNotices(limit = 20) {
|
|
38
80
|
const node = useSynapse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapse-sdk/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Synapse SDK React hooks: connection, topics, calls, services, store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@synapse-sdk/core": "0.3.
|
|
18
|
-
"@synapse-sdk/client": "0.3.
|
|
17
|
+
"@synapse-sdk/core": "0.3.7",
|
|
18
|
+
"@synapse-sdk/client": "0.3.7"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"react": ">=18"
|