@synapse-sdk/react 0.3.0 → 0.3.3
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 +63 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +31 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,3 +1,65 @@
|
|
|
1
1
|
# @synapse-sdk/react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React hooks for a Synapse node: `<SynapseProvider node={node}>` above your components, then `useConnection()`,
|
|
4
|
+
`useProject()`, `useCatalog()`, `useTopic(t, { select })`, `useCall(svc)`, `useStart(svc)`, `useServe(svc, handler)`,
|
|
5
|
+
`usePublisher(t)`, `useStore(name)`, `useScene()`, `useSceneEntity(scene, id, { select, plane })`, `useSceneTree(scene)`,
|
|
6
|
+
`useSceneStatus(scene)` and `useTasks(select?)`. Every hook reads a value the node keeps current; none keeps its own
|
|
7
|
+
copy.
|
|
8
|
+
|
|
9
|
+
## Tasks
|
|
10
|
+
|
|
11
|
+
`useTasks()` is one view: `status` (what runs, `node.tasks.status.value`), `catalog` (what the runtime offers), `synced`,
|
|
12
|
+
and the four verbs; the view is the same object while none of the three values changed. `useTasks(select)` is what
|
|
13
|
+
`select` picks from that view, and the component re-renders only when the selection changes (an unchanged selection
|
|
14
|
+
keeps its object). A button that only runs and stops needs no subscription: it calls the verbs on the node.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
function RunPill() {
|
|
18
|
+
const node = useSynapse();
|
|
19
|
+
const state = useTasks(t => t.status.project.state); // re-renders on the project run's state only
|
|
20
|
+
const [note, setNote] = useState<string>();
|
|
21
|
+
const run = () => node.tasks.runProjectTasks().then(
|
|
22
|
+
r => setNote(`${r.launched} running · ${r.passive} passive`),
|
|
23
|
+
e => setNote(isTasksRefusal(e) ? e.data.report.map(i => `${i.entity_name ?? i.instance}: ${i.reason}`).join("\n") : e.code));
|
|
24
|
+
if (state === "running" || state === "stopping")
|
|
25
|
+
return <button disabled={state === "stopping"} onClick={() => node.tasks.stopProjectTasks()}>{state === "stopping" ? "stopping" : "running · stop"}</button>;
|
|
26
|
+
return <><button disabled={state !== "stopped"} onClick={run}>{state === "starting" ? "starting" : "run"}</button>{note}</>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// a per-entity dot: undefined = not running
|
|
30
|
+
const sim = useTasks(t => t.status.project.tasks[entityId]?.sim?.state);
|
|
31
|
+
|
|
32
|
+
// the picker: every task the runtime offers, by address
|
|
33
|
+
const { catalog, synced } = useTasks();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The node must be connected when the hook first renders (`node.tasks` throws `not_connected` before).
|
|
37
|
+
|
|
38
|
+
## 3D reads, HTML renders
|
|
39
|
+
|
|
40
|
+
3D never renders through React. A frame loop (`useFrame`, or any engine loop) reads the current value imperatively,
|
|
41
|
+
from a view made once, and compares with `===`: the values are structurally shared, so a part that did not change is
|
|
42
|
+
the same object and the loop can skip it. No React state, no re-render, no hook per entity. Hooks with `select` are
|
|
43
|
+
for the HTML side: they wake a component only when its selection changes.
|
|
44
|
+
|
|
45
|
+
| what | in the frame loop (no render) | hook for HTML (wakes on) |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| one entity | `const v = useMemo(() => scene.entity(id), [scene, id])`; `v.get()` | `useSceneEntity(scene, id, { select })` (the selection changing) |
|
|
48
|
+
| a topic | `const v = useMemo(() => node.view(t), [t])` (close it on unmount); `v.get().data` | `useTopic(t, { select })` (the selection changing) |
|
|
49
|
+
| a namespace | `store.value` | `useStore(name)` (any change of the namespace) |
|
|
50
|
+
| tasks | `node.tasks.status.value` | `useTasks(select)` (the selection changing) |
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
function Arm({ scene, id }: { scene: SceneHandle; id: string }) {
|
|
54
|
+
const ref = useRef<THREE.Group>(null);
|
|
55
|
+
const view = useMemo(() => scene.entity(id), [scene, id]); // made once
|
|
56
|
+
const last = useRef<SceneEntity | undefined>(undefined);
|
|
57
|
+
useFrame(() => {
|
|
58
|
+
const e = view.get(); // the value now: no render
|
|
59
|
+
if (e === last.current || !e?.xform) return; // unchanged: the same object
|
|
60
|
+
last.current = e;
|
|
61
|
+
ref.current!.position.fromArray(e.xform.t ?? [0, 0, 0]);
|
|
62
|
+
});
|
|
63
|
+
return <group ref={ref} />;
|
|
64
|
+
}
|
|
65
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import type { Synapse, Catalog, ConnectionState, Notice, ViewOptions, ViewState, Run, CallOptions, Publisher, ServeHandler, ServeOptions, Registration, StoreHandle, StoreOptions, SceneEntity, SceneEntityLive, SceneHandle, SceneStatus, SceneTree } from "@synapse-sdk/client";
|
|
2
|
+
import type { Synapse, Catalog, ConnectionState, Notice, ViewOptions, ViewState, Run, CallOptions, Publisher, ServeHandler, ServeOptions, Registration, StoreHandle, StoreOptions, SceneEntity, SceneEntityLive, 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
|
export declare function SynapseProvider({ node, children }: {
|
|
5
5
|
node: Synapse;
|
|
@@ -102,3 +102,25 @@ export declare function useSceneEntity<S = SceneEntityLive | undefined>(scene: S
|
|
|
102
102
|
export declare function useSceneTree(scene: SceneHandle | null | undefined): SceneTree;
|
|
103
103
|
/** The scene handle's status (`scene.status`), re-rendering when a field changes; `undefined` without a scene. */
|
|
104
104
|
export declare function useSceneStatus(scene: SceneHandle | null | undefined): SceneStatus | undefined;
|
|
105
|
+
/** What `useTasks` hands back: the facet's three values as the node keeps them current, and its verbs (stable for the node's life). */
|
|
106
|
+
export interface TasksView {
|
|
107
|
+
readonly status: TasksStatus;
|
|
108
|
+
/** By address. */
|
|
109
|
+
readonly catalog: Readonly<Record<string, TaskCatalogEntry>>;
|
|
110
|
+
/** Both `status` and `catalog` current. */
|
|
111
|
+
readonly synced: boolean;
|
|
112
|
+
runRobotTasks(): Promise<TasksRun>;
|
|
113
|
+
stopRobotTasks(): Promise<void>;
|
|
114
|
+
runProjectTasks(): Promise<TasksRun>;
|
|
115
|
+
stopProjectTasks(): Promise<void>;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The node's tasks: `status` (what runs), `catalog` (what the runtime offers) and `synced`, with the verbs, as one
|
|
119
|
+
* `TasksView`; or what `select` picks from it. The view is the same object while none of the three values changed.
|
|
120
|
+
* `select` runs here with the selector of the latest render; a selected value deep-equal to the previous one keeps its
|
|
121
|
+
* reference, so a change elsewhere (another task's restart, a catalog refresh) does not re-render the component.
|
|
122
|
+
* A component that only runs and stops needs no subscription: `useSynapse().tasks.runProjectTasks()`.
|
|
123
|
+
* The provider's node must be connected (`node.tasks` throws `not_connected` before).
|
|
124
|
+
*/
|
|
125
|
+
export declare function useTasks(): TasksView;
|
|
126
|
+
export declare function useTasks<S>(select: (t: TasksView) => S): S;
|
package/dist/index.js
CHANGED
|
@@ -371,3 +371,34 @@ export function useSceneStatus(scene) {
|
|
|
371
371
|
}, [scene]);
|
|
372
372
|
return useSyncExternalStore(subscribe, get, get);
|
|
373
373
|
}
|
|
374
|
+
export function useTasks(select) {
|
|
375
|
+
const node = useSynapse();
|
|
376
|
+
const tasks = node.tasks;
|
|
377
|
+
const selectRef = useRef(select);
|
|
378
|
+
selectRef.current = select;
|
|
379
|
+
const subscribe = useCallback((cb) => {
|
|
380
|
+
const a = tasks.status.subscribe(cb), b = tasks.catalog.subscribe(cb);
|
|
381
|
+
return () => { a(); b(); };
|
|
382
|
+
}, [tasks]);
|
|
383
|
+
const verbs = useMemo(() => ({
|
|
384
|
+
runRobotTasks: () => tasks.runRobotTasks(), stopRobotTasks: () => tasks.stopRobotTasks(),
|
|
385
|
+
runProjectTasks: () => tasks.runProjectTasks(), stopProjectTasks: () => tasks.stopProjectTasks(),
|
|
386
|
+
}), [tasks]);
|
|
387
|
+
const view = useRef(undefined);
|
|
388
|
+
const last = useRef(undefined);
|
|
389
|
+
const get = useCallback(() => {
|
|
390
|
+
const status = tasks.status.value, catalog = tasks.catalog.value, synced = tasks.status.synced && tasks.catalog.synced;
|
|
391
|
+
const v = view.current;
|
|
392
|
+
const from = v && v.status === status && v.catalog === catalog && v.synced === synced ? v : (view.current = { status, catalog, synced, ...verbs });
|
|
393
|
+
const sel = selectRef.current;
|
|
394
|
+
if (!sel)
|
|
395
|
+
return from;
|
|
396
|
+
const prev = last.current;
|
|
397
|
+
if (prev && prev.from === from && prev.select === sel)
|
|
398
|
+
return prev.out;
|
|
399
|
+
const out = replaceEqualDeep(prev?.out, sel(from));
|
|
400
|
+
last.current = { from, select: sel, out };
|
|
401
|
+
return out;
|
|
402
|
+
}, [tasks, verbs]);
|
|
403
|
+
return useSyncExternalStore(subscribe, get, get);
|
|
404
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synapse-sdk/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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.3",
|
|
18
|
+
"@synapse-sdk/client": "0.3.3"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"react": ">=18"
|