@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 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)` and `useTasks(select?)`. Every hook reads a value the node keeps current; none keeps its own
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
- export declare function SynapseProvider({ node, children }: {
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
- export function SynapseProvider({ node, children }) {
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.4",
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.4",
18
- "@synapse-sdk/client": "0.3.4"
17
+ "@synapse-sdk/core": "0.3.7",
18
+ "@synapse-sdk/client": "0.3.7"
19
19
  },
20
20
  "peerDependencies": {
21
21
  "react": ">=18"