@noya-app/noya-multiplayer-react 0.1.23 → 0.1.25

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/src/hooks.ts CHANGED
@@ -6,16 +6,18 @@ import {
6
6
  EphemeralUserData,
7
7
  Get,
8
8
  MultiplayerStateManager,
9
+ MultiplayerStateManagerError,
9
10
  MultiplayerStateManagerOptions,
10
11
  MultiplayerUser,
11
12
  StateManager,
12
13
  StateManagerOptions,
14
+ stubSync,
13
15
  SyncAdapter,
14
16
  SyncAdapterOptions,
15
17
  Task,
16
- stubSync,
17
18
  } from "@noya-app/state-manager";
18
19
  import {
20
+ createElement,
19
21
  SetStateAction,
20
22
  useCallback,
21
23
  useEffect,
@@ -24,19 +26,19 @@ import {
24
26
  useState,
25
27
  useSyncExternalStore,
26
28
  } from "react";
29
+ import { createRoot } from "react-dom/client";
30
+ import { ErrorOverlay } from "./components/ErrorOverlay";
27
31
  import { shouldTrackEvents$, shouldTrackTasks$ } from "./globals";
28
32
  import { StateInspectorOptions } from "./inspector/StateInspector";
29
33
  import { useStateInspector } from "./inspector/useStateInspector";
30
34
  import { useObservable } from "./useObservable";
31
35
 
32
- export function useSyncStateManager<S extends object, M>(
33
- stateManager: StateManager<S, M>
34
- ): S;
35
- export function useSyncStateManager<S extends object, M, P extends string>(
36
+ export function useSyncStateManager<S, M>(stateManager: StateManager<S, M>): S;
37
+ export function useSyncStateManager<S, M, P extends string>(
36
38
  stateManager: StateManager<S, M>,
37
39
  path: P
38
40
  ): Get<S, P, unknown>;
39
- export function useSyncStateManager<S extends object, M>(
41
+ export function useSyncStateManager<S, M>(
40
42
  stateManager: StateManager<S, M>,
41
43
  path?: string
42
44
  ) {
@@ -55,7 +57,7 @@ export function useSyncStateManager<S extends object, M>(
55
57
  );
56
58
  }
57
59
 
58
- export function useManagedState<S extends object, M = void>(
60
+ export function useManagedState<S, M = void>(
59
61
  createInitialState: () => S,
60
62
  options?: StateManagerOptions<S, M>
61
63
  ) {
@@ -79,7 +81,7 @@ export function useManagedState<S extends object, M = void>(
79
81
  handler: ActionHandler<S>
80
82
  ) => void;
81
83
 
82
- if (typeof action === "function") {
84
+ if (action instanceof Function) {
83
85
  performActionNoMetadata({
84
86
  run: action,
85
87
  undo: () => state,
@@ -96,7 +98,7 @@ export function useManagedState<S extends object, M = void>(
96
98
  handler: ActionHandler<S>
97
99
  ) => void;
98
100
 
99
- if (typeof action === "function") {
101
+ if (action instanceof Function) {
100
102
  performActionWithMetadata(metadata, {
101
103
  run: action,
102
104
  undo: () => state,
@@ -115,7 +117,7 @@ export function useManagedState<S extends object, M = void>(
115
117
  return [state, setState, stateManager] as const;
116
118
  }
117
119
 
118
- export function useManagedHistory<S extends object, M = void>(
120
+ export function useManagedHistory<S, M = void>(
119
121
  stateManager: StateManager<S, M>
120
122
  ) {
121
123
  return useSyncExternalStore(
@@ -125,7 +127,7 @@ export function useManagedHistory<S extends object, M = void>(
125
127
  );
126
128
  }
127
129
 
128
- export function useSyncMultiplayerStateManager<S extends object, M = void>(
130
+ export function useSyncMultiplayerStateManager<S, M = void>(
129
131
  multiplayerStateManager: MultiplayerStateManager<S, M>
130
132
  ) {
131
133
  return useSyncExternalStore(
@@ -135,11 +137,52 @@ export function useSyncMultiplayerStateManager<S extends object, M = void>(
135
137
  );
136
138
  }
137
139
 
140
+ function throttle<F extends (...args: any[]) => any>(fn: F, ms: number) {
141
+ let lastCall = 0;
142
+ let timeoutId: number | null = null;
143
+ return (...args: Parameters<F>) => {
144
+ const now = Date.now();
145
+ if (now - lastCall < ms) {
146
+ if (timeoutId) clearTimeout(timeoutId);
147
+ timeoutId = setTimeout(
148
+ () => {
149
+ lastCall = now;
150
+ fn(...args);
151
+ },
152
+ ms - (now - lastCall)
153
+ ) as unknown as number;
154
+ return;
155
+ }
156
+ lastCall = now;
157
+ return fn(...args);
158
+ };
159
+ }
160
+
138
161
  export function useEphemeralUserData<E>(
139
- ephemeralUserData: EphemeralUserData<E>
162
+ ephemeralUserData: EphemeralUserData<E>,
163
+ options?: {
164
+ throttle?: number;
165
+ }
140
166
  ) {
167
+ const throttledAddListener = useMemo(() => {
168
+ const throttleMs = options?.throttle ?? 0;
169
+
170
+ if (throttleMs > 0) {
171
+ const addListener = (
172
+ fn: Parameters<typeof ephemeralUserData.addListener>[0]
173
+ ) => {
174
+ const throttledFn = throttle(fn, throttleMs);
175
+ return ephemeralUserData.addListener(throttledFn);
176
+ };
177
+
178
+ return addListener;
179
+ }
180
+
181
+ return ephemeralUserData.addListener;
182
+ }, [ephemeralUserData, options?.throttle]);
183
+
141
184
  return useSyncExternalStore(
142
- ephemeralUserData.addListener,
185
+ throttledAddListener,
143
186
  ephemeralUserData.getSnapshot,
144
187
  ephemeralUserData.getSnapshot
145
188
  );
@@ -148,7 +191,7 @@ export function useEphemeralUserData<E>(
148
191
  const defaultStubSync = stubSync();
149
192
 
150
193
  export type UseMultiplayerStateOptions<
151
- S extends object,
194
+ S,
152
195
  M = void,
153
196
  E = void,
154
197
  > = MultiplayerStateManagerOptions<S, M> & {
@@ -160,7 +203,7 @@ export type UseMultiplayerStateOptions<
160
203
  inspector?: boolean | StateInspectorOptions;
161
204
  };
162
205
 
163
- export function useMultiplayerState<S extends object, M = void, E = void>(
206
+ export function useMultiplayerState<S, M = void, E = void>(
164
207
  initialState: S | (() => S),
165
208
  options?: UseMultiplayerStateOptions<S, M, E>
166
209
  ) {
@@ -271,12 +314,35 @@ export function useMultiplayerState<S extends object, M = void, E = void>(
271
314
 
272
315
  const forceInit = useCallback(() => {
273
316
  const state =
274
- typeof initialState === "function" ? initialState() : initialState;
317
+ initialState instanceof Function ? initialState() : initialState;
275
318
  const schema = options?.schema;
276
319
  multiplayerStateManager.sm.clearHistory();
277
320
  multiplayerStateManager.sendInit({ force: true, state, schema });
278
321
  }, [initialState, multiplayerStateManager, options?.schema]);
279
322
 
323
+ const [unrecoverableError, setUnrecoverableError] = useState<
324
+ MultiplayerStateManagerError | undefined
325
+ >();
326
+
327
+ useEffect(() => {
328
+ return multiplayerStateManager.errorEmitter.addListener((error) => {
329
+ setUnrecoverableError(error);
330
+ });
331
+ }, [multiplayerStateManager]);
332
+
333
+ useEffect(() => {
334
+ if (!unrecoverableError) return;
335
+ const el = document.createElement("div");
336
+ el.id = "noya-multiplayer-react-error-overlay";
337
+ document.body.appendChild(el);
338
+ const root = createRoot(el);
339
+ root.render(createElement(ErrorOverlay, { error: unrecoverableError }));
340
+ return () => {
341
+ root.unmount();
342
+ el.remove();
343
+ };
344
+ });
345
+
280
346
  useStateInspector({
281
347
  ephemeralUserData,
282
348
  multiplayerStateManager,
package/src/index.ts CHANGED
@@ -4,3 +4,4 @@ export * from "./components/UserPointersOverlay";
4
4
  export * from "./hooks";
5
5
  export * from "./inspector/StateInspector";
6
6
  export * from "./noyaApp";
7
+ export * from "./useObservable";
@@ -249,11 +249,7 @@ export type StateInspectorOptions = {
249
249
  anchor?: StateInspectorAnchor;
250
250
  };
251
251
 
252
- export const StateInspector = memo(function StateInspector<
253
- S extends object,
254
- M,
255
- E,
256
- >({
252
+ export const StateInspector = memo(function StateInspector<S, M, E>({
257
253
  state,
258
254
  connectionEvents,
259
255
  connectedUsers,
@@ -757,7 +753,7 @@ export const StateInspector = memo(function StateInspector<
757
753
  }}
758
754
  >
759
755
  <ObjectInspector
760
- data={event.message}
756
+ data={event.type === "error" ? event.error : event.message}
761
757
  theme={theme}
762
758
  nodeRenderer={({
763
759
  depth,
@@ -19,7 +19,7 @@ function createPortalElement() {
19
19
  return el;
20
20
  }
21
21
 
22
- export function useStateInspector<S extends object, M, E>({
22
+ export function useStateInspector<S, M, E>({
23
23
  state,
24
24
  connectionEvents,
25
25
  connectedUsers,
package/src/noyaApp.ts CHANGED
@@ -63,13 +63,13 @@ type AppDataOptions = {
63
63
  /**
64
64
  * Get data from host app. Fall back to default data.
65
65
  */
66
- export function useAppData<S extends object>(
66
+ export function useAppData<S>(
67
67
  initialState: S,
68
68
  { schema }: AppDataOptions = {}
69
69
  ) {
70
70
  const appData = useMemo(() => {
71
71
  return getAppData(
72
- schema ? createValue(schema, initialState) : initialState
72
+ schema ? (createValue(schema, initialState) as S) : initialState
73
73
  );
74
74
  }, [initialState, schema]);
75
75
 
@@ -142,7 +142,7 @@ export function isEmbeddedApp() {
142
142
  return window.self !== window.top;
143
143
  }
144
144
 
145
- export function useNoyaAppState<S extends object, M = void, E = void>(
145
+ export function useNoyaAppState<S, M = void, E = void>(
146
146
  initialState: S | (() => S),
147
147
  options: UseMultiplayerStateOptions<S, M, E> & {
148
148
  localStorageKey?: string;