openrtc-netcode-react 3.0.1 → 3.0.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/README.md CHANGED
@@ -1,21 +1,31 @@
1
1
  # openrtc-netcode-react
2
2
 
3
- React context and hooks for `openrtc-netcode`.
3
+ React context and hooks for an explicitly activated `openrtc-netcode` client.
4
4
 
5
5
  ```tsx
6
- import { NetcodeProvider, usePeers, useNetVar } from 'openrtc-netcode-react';
6
+ import { NetcodeProvider, useNetVar } from 'openrtc-netcode-react';
7
7
 
8
8
  function Score() {
9
- const peers = usePeers();
10
9
  const [score, setScore] = useNetVar('score', 0);
11
- return <button onClick={() => setScore(score + 1)}>{peers.length}: {score}</button>;
10
+ return <button onClick={() => void setScore(score + 1)}>{score}</button>;
12
11
  }
13
-
14
- export function App() {
15
- return (
16
- <NetcodeProvider options={{ apiKey: 'pk_live_...' }}>
17
- <Score />
18
- </NetcodeProvider>
19
- );
12
+ // Construct and join client with your activated OpenRTC Room first.
13
+ export function Game({ client }) {
14
+ return <NetcodeProvider client={client} autoStart={false}><Score /></NetcodeProvider>;
20
15
  }
21
16
  ```
17
+
18
+ `useNetVar` follows key/client changes and state reset without retaining retired
19
+ match handles. Ownership still applies; a non-owner setter rejects. Provider
20
+ auto-start errors go to `onError` (or console.error), including cleanup errors.
21
+ Keep a supplied client and options stable across ordinary renders.
22
+
23
+ For high-frequency 3D rendering, use `useNetworkObjectIds(kind)` for structure
24
+ and `client.objects.read(id)` in the engine frame loop; sample a SnapshotBuffer
25
+ into pooled vectors. This avoids collection clones and React renders for motion.
26
+ `useNetworkObject(id)` and `useNetworkObjects(kind)` suit lower-frequency UI.
27
+ Objects returned by imperative read and hooks are deeply frozen. Use object
28
+ mutation APIs rather than changing nested fields.
29
+
30
+ Wire v2 requires the coordinated major release; see core netcode migration,
31
+ authority, bounds and validation documentation.
package/dist/index.d.ts CHANGED
@@ -4,14 +4,18 @@ export interface NetcodeProviderProps extends PropsWithChildren {
4
4
  client?: NetcodeClient;
5
5
  options?: NetcodeOptions;
6
6
  autoStart?: boolean;
7
+ onError?: (error: Error) => void;
7
8
  }
8
- export declare function NetcodeProvider({ client, options, autoStart, children }: NetcodeProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<NetcodeClient | null>>;
9
+ export declare function NetcodeProvider({ client, options, autoStart, onError, children }: NetcodeProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<NetcodeClient | null>>;
9
10
  export declare function useNetcode(): NetcodeClient;
10
11
  export declare function useLobby(): NetcodeLobby | null;
11
12
  export declare function usePeers(): NetcodePeer[];
12
13
  export declare function usePeerStats(): NetcodePeerStats[];
13
14
  export declare function useNetVar<T>(key: string, initial: T, options?: NetVarOptions): [T, (value: T) => Promise<void>];
14
15
  export declare function useNetworkObjects(kind?: string): NetcodeObjectState[];
16
+ /** Entity presence only. Motion is read imperatively without React rerenders. */
17
+ export declare function useNetworkObjectIds(kind?: string): readonly string[];
18
+ export declare function useNetworkObject(id: string): Readonly<NetcodeObjectState> | undefined;
15
19
  export declare function useNetcodeVoicePeers(): NetcodeVoicePeer[];
16
20
  export declare function useNetcodeEvent<K extends NetcodeEventName>(event: K, handler: (payload: NetcodeEvents[K]) => void): void;
17
21
  export declare function useNetcodeMessage<T = unknown>(type: string, handler: MessageHandler<T>): void;
package/dist/index.js CHANGED
@@ -1,15 +1,24 @@
1
- import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
2
2
  import { createNetcode, } from 'openrtc-netcode';
3
3
  const NetcodeContext = createContext(null);
4
- export function NetcodeProvider({ client, options, autoStart = true, children }) {
4
+ export function NetcodeProvider({ client, options, autoStart = true, onError, children }) {
5
5
  const resolvedClient = useMemo(() => client ?? createNetcode(options), [client, options]);
6
+ const errorHandler = useRef(onError);
7
+ errorHandler.current = onError;
6
8
  useEffect(() => {
7
9
  if (!autoStart) {
8
10
  return undefined;
9
11
  }
10
- void resolvedClient.start();
12
+ const reportError = (error) => {
13
+ const reason = error instanceof Error ? error : new Error(String(error));
14
+ if (errorHandler.current)
15
+ errorHandler.current(reason);
16
+ else
17
+ console.error('[openrtc-netcode-react] client lifecycle failed', reason);
18
+ };
19
+ void resolvedClient.start().catch(reportError);
11
20
  return () => {
12
- void resolvedClient.stop();
21
+ void resolvedClient.stop().catch(reportError);
13
22
  };
14
23
  }, [autoStart, resolvedClient]);
15
24
  return createElement(NetcodeContext.Provider, { value: resolvedClient }, children);
@@ -24,7 +33,11 @@ export function useNetcode() {
24
33
  export function useLobby() {
25
34
  const client = useNetcode();
26
35
  const [lobby, setLobby] = useState(() => client.lobby);
27
- useEffect(() => client.on('lobby', setLobby), [client]);
36
+ useEffect(() => {
37
+ const stop = client.on('lobby', setLobby);
38
+ setLobby(client.lobby);
39
+ return stop;
40
+ }, [client]);
28
41
  return lobby;
29
42
  }
30
43
  export function usePeers() {
@@ -71,22 +84,70 @@ export function usePeerStats() {
71
84
  }
72
85
  export function useNetVar(key, initial, options) {
73
86
  const client = useNetcode();
74
- const netVar = useMemo(() => client.state.define(key, initial, options), [client, key]);
75
- const [value, setValue] = useState(() => netVar.value);
76
- useEffect(() => netVar.onChange((next) => setValue(next)), [netVar]);
77
- const setNetVar = useCallback((next) => netVar.set(next), [netVar]);
87
+ const store = useMemo(() => {
88
+ client.state.define(key, initial, options);
89
+ const read = () => {
90
+ const value = client.state.get(key);
91
+ return value === undefined ? initial : value;
92
+ };
93
+ let snapshot = read();
94
+ return {
95
+ getSnapshot: () => snapshot,
96
+ subscribe: (notify) => {
97
+ const stop = client.state.subscribe(key, () => { snapshot = read(); notify(); });
98
+ snapshot = read();
99
+ return stop;
100
+ },
101
+ set: (next) => client.state.define(key, initial, options).set(next),
102
+ };
103
+ }, [client, key, options?.owner]);
104
+ const value = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
105
+ const setNetVar = useCallback((next) => store.set(next), [store]);
78
106
  return [value, setNetVar];
79
107
  }
80
108
  export function useNetworkObjects(kind) {
81
109
  const client = useNetcode();
82
110
  const [objects, setObjects] = useState(() => filterObjects(client.objects.list(), kind));
83
111
  useEffect(() => {
84
- const refresh = () => setObjects(filterObjects(client.objects.list(), kind));
112
+ const refresh = () => setObjects(client.objects.ids(kind)
113
+ .map((id) => client.objects.read(id)));
85
114
  refresh();
86
- return client.objects.onChange(refresh);
115
+ return client.objects.onChange((change) => {
116
+ if (change.type === 'snapshot')
117
+ return refresh();
118
+ setObjects((previous) => {
119
+ const id = change.type === 'remove' ? change.id : change.object.id;
120
+ const object = change.type === 'upsert' ? client.objects.read(id) : undefined;
121
+ const index = previous.findIndex((entry) => entry.id === id);
122
+ if (!object || (kind && object.kind !== kind)) {
123
+ return index < 0 ? previous : previous.filter((entry) => entry.id !== id);
124
+ }
125
+ if (index < 0)
126
+ return [...previous, object];
127
+ const next = previous.slice();
128
+ next[index] = object;
129
+ return next;
130
+ });
131
+ });
87
132
  }, [client, kind]);
88
133
  return objects;
89
134
  }
135
+ /** Entity presence only. Motion is read imperatively without React rerenders. */
136
+ export function useNetworkObjectIds(kind) {
137
+ const client = useNetcode();
138
+ const subscribe = useCallback((notify) => client.objects.onStructureChange(notify), [client]);
139
+ const snapshot = useCallback(() => client.objects.ids(kind), [client, kind]);
140
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
141
+ }
142
+ export function useNetworkObject(id) {
143
+ const client = useNetcode();
144
+ const subscribe = useCallback((notify) => client.objects.onChange((change) => {
145
+ if (change.type === 'snapshot' || (change.type === 'remove' ? change.id : change.object.id) === id)
146
+ notify();
147
+ }), [client, id]);
148
+ const snapshot = useCallback(() => client.objects.read(id), [client, id]);
149
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
150
+ }
90
151
  export function useNetcodeVoicePeers() {
91
152
  const client = useNetcode();
92
153
  const [peers, setPeers] = useState(() => client.voice.listPeers());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openrtc-netcode-react",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -40,7 +40,7 @@
40
40
  "test:watch": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "openrtc-netcode": "^3.0.1"
43
+ "openrtc-netcode": "^3.0.2"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": "^18.2.0 || ^19.0.0"