@noya-app/noya-multiplayer-react 0.1.12 → 0.1.13

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
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  ActionHandler,
3
5
  ConnectionEvent,
@@ -8,6 +10,7 @@ import {
8
10
  StateManager,
9
11
  StateManagerOptions,
10
12
  SyncAdapter,
13
+ Task,
11
14
  stubSync,
12
15
  } from "@noya-app/state-manager";
13
16
  import {
@@ -20,7 +23,11 @@ import {
20
23
  useState,
21
24
  useSyncExternalStore,
22
25
  } from "react";
23
- import { trackEvents, useObservable } from "./globals";
26
+ import {
27
+ shouldTrackEvents$,
28
+ shouldTrackTasks$,
29
+ useObservable,
30
+ } from "./globals";
24
31
  import { StateInspector } from "./inspector/StateInspector";
25
32
  import { useStateInspector } from "./inspector/useStateInspector";
26
33
 
@@ -134,21 +141,21 @@ const defaultStubSync = stubSync();
134
141
 
135
142
  export function useMultiplayerState<S extends object, M = void>(
136
143
  initialState: S | (() => S),
137
- options: MultiplayerStateManagerOptions<S, M> & {
144
+ options?: MultiplayerStateManagerOptions<S, M> & {
138
145
  sync?: (options: { ms: MultiplayerStateManager<S, M> }) => void;
139
146
  trackConnectionEvents?: boolean;
147
+ trackTasks?: boolean;
140
148
  inspector?:
141
149
  | boolean
142
150
  | Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">;
143
151
  }
144
152
  ) {
145
153
  const { sync = defaultStubSync as unknown as SyncAdapter<S, M>, ...rest } =
146
- options;
154
+ options ?? {};
147
155
 
148
156
  const [multiplayerStateManager] = useState(
149
157
  () => new MultiplayerStateManager<S, M>(initialState, rest)
150
158
  );
151
-
152
159
  const syncRef = useRef(sync);
153
160
 
154
161
  const [connectionEvents, setConnectionEvents] = useState<
@@ -156,43 +163,69 @@ export function useMultiplayerState<S extends object, M = void>(
156
163
  >();
157
164
 
158
165
  const [connectedUsers, setConnectedUsers] = useState<MultiplayerUser[]>([]);
166
+ const [tasks, setTasks] = useState<Task[]>([]);
159
167
  const [userId, setUserId] = useState<string | undefined>();
160
168
 
161
169
  const extras = useMemo(() => {
162
170
  return {
171
+ tasks,
163
172
  connectionEvents,
164
173
  connectedUsers,
174
+ // evaluate: async (code: string) => {
175
+ // const payload = await rpcManager.request({ type: "eval", code });
176
+
177
+ // if (payload.type !== "eval") {
178
+ // throw new Error(`Unexpected rpc response type: ${payload.type}`);
179
+ // }
180
+ // },
165
181
  };
166
- }, [connectionEvents, connectedUsers]);
182
+ }, [tasks, connectionEvents, connectedUsers]);
167
183
 
168
- const globalTrackEvents = useObservable(trackEvents);
169
- const trackConnectionEvents =
170
- options.trackConnectionEvents ?? globalTrackEvents;
184
+ const globalTrackEvents = useObservable(shouldTrackEvents$);
185
+ const globalTrackTasks = useObservable(shouldTrackTasks$);
186
+
187
+ const shouldTrackEvents = options?.trackConnectionEvents ?? globalTrackEvents;
188
+ const shouldTrackTasks = options?.trackTasks ?? globalTrackTasks;
171
189
 
172
190
  const trackEventsCallbackRef = useRef<
173
191
  ((e: ConnectionEvent<S>) => void) | undefined
174
192
  >();
175
-
176
193
  const trackConnectedUsersCallbackRef = useRef<
177
194
  ((users: MultiplayerUser[], userId: string) => void) | undefined
178
195
  >();
196
+ const trackTasksCallbackRef = useRef<((tasks: Task[]) => void) | undefined>();
179
197
 
180
198
  useEffect(() => {
181
- trackEventsCallbackRef.current = trackConnectionEvents
199
+ trackEventsCallbackRef.current = shouldTrackEvents
182
200
  ? (e) =>
183
201
  setConnectionEvents((events) => {
184
202
  const updated = events ? [...events, e] : [e];
185
203
  return updated.length > 50 ? updated.slice(-50) : updated;
186
204
  })
187
205
  : undefined;
188
- }, [trackConnectionEvents]);
206
+ }, [shouldTrackEvents]);
189
207
 
190
208
  useEffect(() => {
191
209
  trackConnectedUsersCallbackRef.current = (users, userId) => {
192
210
  setConnectedUsers(users);
193
211
  setUserId(userId);
194
212
  };
195
- });
213
+ }, []);
214
+
215
+ useEffect(() => {
216
+ trackTasksCallbackRef.current = shouldTrackTasks
217
+ ? (tasks) => {
218
+ setTasks((old) => {
219
+ const newIds = new Set(tasks.map((t) => t.id));
220
+
221
+ // merge old and new tasks, preferring new tasks
222
+ const unchanged = old.filter((t) => !newIds.has(t.id));
223
+
224
+ return [...unchanged, ...tasks];
225
+ });
226
+ }
227
+ : undefined;
228
+ }, [shouldTrackTasks]);
196
229
 
197
230
  useEffect(() => {
198
231
  if (syncRef.current) {
@@ -201,6 +234,7 @@ export function useMultiplayerState<S extends object, M = void>(
201
234
  onConnectionEvent: (e) => trackEventsCallbackRef.current?.(e),
202
235
  onChangeConnectedUsers: (users, userId) =>
203
236
  trackConnectedUsersCallbackRef.current?.(users, userId),
237
+ onChangeTasks: (tasks) => trackTasksCallbackRef.current?.(tasks),
204
238
  });
205
239
  }
206
240
  }, [multiplayerStateManager]);
@@ -208,18 +242,19 @@ export function useMultiplayerState<S extends object, M = void>(
208
242
  const state = useSyncMultiplayerStateManager(multiplayerStateManager);
209
243
 
210
244
  useStateInspector({
211
- stateManager: multiplayerStateManager.sm,
245
+ multiplayerStateManager,
212
246
  state,
213
247
  connectionEvents,
214
248
  connectedUsers,
249
+ tasks,
215
250
  userId,
216
- disabled: !options.inspector,
217
- ...(typeof options.inspector === "object" && options.inspector),
251
+ disabled: !options?.inspector,
252
+ ...(typeof options?.inspector === "object" && options.inspector),
218
253
  });
219
254
 
220
255
  return [
221
256
  state,
222
- multiplayerStateManager.setOptimisticState,
257
+ multiplayerStateManager.setState,
223
258
  multiplayerStateManager,
224
259
  extras,
225
260
  ] as const;
@@ -1,8 +1,11 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  ConnectionEvent,
3
5
  ExtendedPathKey,
6
+ MultiplayerStateManager,
4
7
  MultiplayerUser,
5
- StateManager,
8
+ Task,
6
9
  } from "@noya-app/state-manager";
7
10
  import React, {
8
11
  CSSProperties,
@@ -20,7 +23,7 @@ import {
20
23
  chromeDark,
21
24
  chromeLight,
22
25
  } from "react-inspector";
23
- import { trackEvents } from "../globals";
26
+ import { shouldTrackEvents$, shouldTrackTasks$ } from "../globals";
24
27
  import { useManagedHistory } from "../hooks";
25
28
  import { useLocalStorageState } from "./useLocalStorageState";
26
29
 
@@ -170,7 +173,7 @@ function DisclosureSection({
170
173
  }}
171
174
  />
172
175
  )}
173
- <span style={{ flex: "1 1 0" }}>{title}</span>
176
+ <span style={{ flex: "1 1 0", userSelect: "none" }}>{title}</span>
174
177
  {right}
175
178
  </div>
176
179
  {open && children}
@@ -234,7 +237,7 @@ function InspectorRow({
234
237
 
235
238
  const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
236
239
 
237
- type Anchor = "left" | "right";
240
+ type Anchor = "left" | "right" | "bottom left" | "bottom right";
238
241
 
239
242
  export const StateInspector = memo(function StateInspector<
240
243
  S extends object,
@@ -243,20 +246,22 @@ export const StateInspector = memo(function StateInspector<
243
246
  state,
244
247
  connectionEvents,
245
248
  connectedUsers,
249
+ tasks,
246
250
  userId,
247
251
  unstyled,
248
252
  colorScheme = "light",
249
- stateManager,
253
+ multiplayerStateManager,
250
254
  anchor = "right",
251
255
  ...props
252
256
  }: {
253
257
  state: S;
254
258
  connectionEvents?: ConnectionEvent<S>[];
255
259
  connectedUsers?: MultiplayerUser[];
260
+ tasks?: Task[];
256
261
  userId?: string;
257
262
  unstyled?: boolean;
258
263
  colorScheme?: "light" | "dark";
259
- stateManager: StateManager<S, M>;
264
+ multiplayerStateManager: MultiplayerStateManager<S, M>;
260
265
  anchor?: Anchor;
261
266
  } & ComponentPropsWithoutRef<"div">) {
262
267
  const [didMount, setDidMount] = React.useState(false);
@@ -288,11 +293,19 @@ export const StateInspector = memo(function StateInspector<
288
293
  "noya-multiplayer-react-show-data",
289
294
  true
290
295
  );
296
+ const [showTasks, setShowTasks] = useLocalStorageState(
297
+ "noya-multiplayer-react-show-tasks",
298
+ false
299
+ );
291
300
 
292
301
  useEffect(() => {
293
- trackEvents.set(showEvents);
302
+ shouldTrackEvents$.set(showEvents);
294
303
  }, [showEvents]);
295
304
 
305
+ useEffect(() => {
306
+ shouldTrackTasks$.set(showTasks);
307
+ }, [showTasks]);
308
+
296
309
  useEffect(() => {
297
310
  if (eventsContainerRef.current) {
298
311
  eventsContainerRef.current.scrollTop =
@@ -300,7 +313,7 @@ export const StateInspector = memo(function StateInspector<
300
313
  }
301
314
  }, [connectionEvents]);
302
315
 
303
- const historySnapshot = useManagedHistory(stateManager);
316
+ const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
304
317
 
305
318
  useEffect(() => {
306
319
  if (historyContainerRef.current) {
@@ -334,7 +347,7 @@ export const StateInspector = memo(function StateInspector<
334
347
  const baseStyle: CSSProperties = {
335
348
  position: "fixed",
336
349
  top: 12,
337
- ...(anchor === "right" ? { right: 12 } : { left: 12 }),
350
+ ...(anchor.includes("right") ? { right: 12 } : { left: 12 }),
338
351
  bottom: 12,
339
352
  width: 400,
340
353
  background:
@@ -361,8 +374,10 @@ export const StateInspector = memo(function StateInspector<
361
374
  style={{
362
375
  ...baseStyle,
363
376
  padding: "4px 10px",
364
- bottom: undefined,
365
377
  width: undefined,
378
+ ...(anchor.includes("bottom")
379
+ ? { bottom: 12, top: undefined }
380
+ : { bottom: undefined }),
366
381
  }}
367
382
  onClick={() => setShowInspector(true)}
368
383
  >
@@ -434,14 +449,23 @@ export const StateInspector = memo(function StateInspector<
434
449
  setOpen={setShowData}
435
450
  open={showData}
436
451
  >
437
- <div
438
- style={{
439
- ...styles.sectionInner,
440
- gap: "1px",
441
- padding: "1px 12px",
442
- }}
443
- >
444
- <ObjectInspector data={state} theme={theme} />
452
+ <div style={styles.sectionInner}>
453
+ <InspectorRow colorScheme={colorScheme}>
454
+ <ObjectInspector
455
+ name={multiplayerStateManager.sm.schema ? "state" : undefined}
456
+ data={state}
457
+ theme={theme}
458
+ />
459
+ </InspectorRow>
460
+ {multiplayerStateManager.sm.schema && (
461
+ <InspectorRow colorScheme={colorScheme}>
462
+ <ObjectInspector
463
+ name="schema"
464
+ data={multiplayerStateManager.sm.schema}
465
+ theme={theme}
466
+ />
467
+ </InspectorRow>
468
+ )}
445
469
  </div>
446
470
  </DisclosureSection>
447
471
  <DisclosureSection
@@ -533,6 +557,35 @@ export const StateInspector = memo(function StateInspector<
533
557
  ))}
534
558
  </div>
535
559
  </DisclosureSection>
560
+ <DisclosureSection
561
+ title="Tasks"
562
+ colorScheme={colorScheme}
563
+ open={showTasks}
564
+ setOpen={setShowTasks}
565
+ >
566
+ <div style={styles.sectionInner}>
567
+ {tasks?.map((task) => (
568
+ <InspectorRow
569
+ key={task.id}
570
+ colorScheme={colorScheme}
571
+ style={{
572
+ backgroundColor:
573
+ task.status === "done"
574
+ ? "rgba(0,255,0,0.2)"
575
+ : task.status === "error"
576
+ ? "rgba(255,0,0,0.2)"
577
+ : undefined,
578
+ }}
579
+ >
580
+ <ObjectInspector
581
+ name={task.name}
582
+ data={task.payload}
583
+ theme={theme}
584
+ />
585
+ </InspectorRow>
586
+ ))}
587
+ </div>
588
+ </DisclosureSection>
536
589
  <DisclosureSection
537
590
  open={showEvents}
538
591
  setOpen={setShowEvents}
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import React from "react";
2
4
 
3
5
  const localStorage = typeof window !== "undefined" ? window.localStorage : null;
@@ -1,7 +1,10 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  ConnectionEvent,
5
+ MultiplayerStateManager,
3
6
  MultiplayerUser,
4
- StateManager,
7
+ Task,
5
8
  } from "@noya-app/state-manager";
6
9
  import React, { ComponentProps, useLayoutEffect } from "react";
7
10
  import { createRoot } from "react-dom/client";
@@ -19,18 +22,20 @@ export function useStateInspector<S extends object, M>({
19
22
  state,
20
23
  connectionEvents,
21
24
  connectedUsers,
25
+ tasks,
22
26
  userId,
23
27
  disabled = false,
24
28
  colorScheme,
25
29
  anchor,
26
- stateManager,
30
+ multiplayerStateManager,
27
31
  }: {
28
32
  state: S;
29
33
  connectionEvents?: ConnectionEvent<S>[];
30
34
  connectedUsers?: MultiplayerUser[];
35
+ tasks?: Task[];
31
36
  userId?: string;
32
37
  disabled: boolean;
33
- stateManager: StateManager<S, M>;
38
+ multiplayerStateManager: MultiplayerStateManager<S, M>;
34
39
  } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
35
40
  const [root, setRoot] = React.useState<Root | null>(null);
36
41
 
@@ -58,10 +63,13 @@ export function useStateInspector<S extends object, M>({
58
63
  state={state}
59
64
  connectionEvents={connectionEvents}
60
65
  connectedUsers={connectedUsers}
66
+ tasks={tasks}
61
67
  userId={userId}
62
68
  colorScheme={colorScheme}
63
69
  anchor={anchor}
64
- stateManager={stateManager as StateManager<any, any>}
70
+ multiplayerStateManager={
71
+ multiplayerStateManager as MultiplayerStateManager<any, any>
72
+ }
65
73
  />
66
74
  );
67
75
  }, [
@@ -71,7 +79,8 @@ export function useStateInspector<S extends object, M>({
71
79
  connectionEvents,
72
80
  root,
73
81
  state,
74
- stateManager,
82
+ multiplayerStateManager,
75
83
  userId,
84
+ tasks,
76
85
  ]);
77
86
  }