@noya-app/noya-multiplayer-react 0.1.11 → 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.
@@ -1,7 +1,11 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  ConnectionEvent,
3
5
  ExtendedPathKey,
4
- StateManager,
6
+ MultiplayerStateManager,
7
+ MultiplayerUser,
8
+ Task,
5
9
  } from "@noya-app/state-manager";
6
10
  import React, {
7
11
  CSSProperties,
@@ -19,7 +23,7 @@ import {
19
23
  chromeDark,
20
24
  chromeLight,
21
25
  } from "react-inspector";
22
- import { trackEvents } from "../globals";
26
+ import { shouldTrackEvents$, shouldTrackTasks$ } from "../globals";
23
27
  import { useManagedHistory } from "../hooks";
24
28
  import { useLocalStorageState } from "./useLocalStorageState";
25
29
 
@@ -35,6 +39,16 @@ const darkTheme = {
35
39
  OBJECT_NAME_COLOR: "rgba(255,255,255,0.7)",
36
40
  } as any;
37
41
 
42
+ const styles = {
43
+ sectionInner: {
44
+ flex: "1 1 0",
45
+ overflowY: "auto",
46
+ overflowX: "hidden",
47
+ display: "flex",
48
+ flexDirection: "column",
49
+ },
50
+ } as const;
51
+
38
52
  function ToggleButton({
39
53
  showInspector,
40
54
  setShowInspector,
@@ -61,7 +75,11 @@ function ToggleButton({
61
75
  justifyContent: "center",
62
76
  padding: "2px 0",
63
77
  }}
64
- onClick={() => setShowInspector(!showInspector)}
78
+ onClick={(event) => {
79
+ event.stopPropagation();
80
+
81
+ setShowInspector(!showInspector);
82
+ }}
65
83
  >
66
84
  <span style={{ width: "12px", height: "12px" }}>
67
85
  {showInspector === (anchor === "right") ? (
@@ -108,6 +126,7 @@ function DisclosureSection({
108
126
  children,
109
127
  colorScheme,
110
128
  isFirst,
129
+ style,
111
130
  }: {
112
131
  open: boolean;
113
132
  setOpen?: (value: boolean) => void;
@@ -116,6 +135,7 @@ function DisclosureSection({
116
135
  children: React.ReactNode;
117
136
  colorScheme: "light" | "dark";
118
137
  isFirst?: boolean;
138
+ style?: CSSProperties;
119
139
  }) {
120
140
  const theme = colorScheme === "light" ? lightTheme : darkTheme;
121
141
 
@@ -128,6 +148,7 @@ function DisclosureSection({
128
148
  flex: open ? "1 1 0" : "0",
129
149
  display: "flex",
130
150
  flexDirection: "column",
151
+ ...style,
131
152
  }}
132
153
  >
133
154
  <div
@@ -152,7 +173,7 @@ function DisclosureSection({
152
173
  }}
153
174
  />
154
175
  )}
155
- <span style={{ flex: "1 1 0" }}>{title}</span>
176
+ <span style={{ flex: "1 1 0", userSelect: "none" }}>{title}</span>
156
177
  {right}
157
178
  </div>
158
179
  {open && children}
@@ -160,9 +181,63 @@ function DisclosureSection({
160
181
  );
161
182
  }
162
183
 
184
+ function InspectorRow({
185
+ children,
186
+ colorScheme,
187
+ selected,
188
+ style,
189
+ variant,
190
+ }: {
191
+ children: React.ReactNode;
192
+ colorScheme: "light" | "dark";
193
+ selected?: boolean;
194
+ style?: CSSProperties;
195
+ variant?: "up" | "down";
196
+ }) {
197
+ const solidBorderColor =
198
+ colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
199
+
200
+ return (
201
+ <div
202
+ style={{
203
+ borderBottom: `1px solid ${solidBorderColor}`,
204
+ fontSize: "12px",
205
+ fontFamily: "Menlo, monospace",
206
+ padding: "2px 12px 1px",
207
+ display: "flex",
208
+ alignItems: "center",
209
+ background:
210
+ variant === "up"
211
+ ? "rgba(0,255,0,0.1)"
212
+ : variant === "down"
213
+ ? "rgba(255,0,0,0.1)"
214
+ : selected
215
+ ? "rgb(59 130 246 / 15%)"
216
+ : undefined,
217
+ // background:
218
+ // colorScheme === "light"
219
+ // ? "rgba(0,0,0,0.05)"
220
+ // : "rgba(255,255,255,0.05)",
221
+ ...style,
222
+ }}
223
+ >
224
+ <span
225
+ style={{
226
+ fontFamily: "Menlo, monospace",
227
+ fontSize: "11px",
228
+ borderRadius: 4,
229
+ display: "inline-block",
230
+ }}
231
+ >
232
+ {children}
233
+ </span>
234
+ </div>
235
+ );
236
+ }
237
+
163
238
  const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
164
239
 
165
- type Anchor = "left" | "right";
240
+ type Anchor = "left" | "right" | "bottom left" | "bottom right";
166
241
 
167
242
  export const StateInspector = memo(function StateInspector<
168
243
  S extends object,
@@ -170,17 +245,23 @@ export const StateInspector = memo(function StateInspector<
170
245
  >({
171
246
  state,
172
247
  connectionEvents,
248
+ connectedUsers,
249
+ tasks,
250
+ userId,
173
251
  unstyled,
174
252
  colorScheme = "light",
175
- stateManager,
253
+ multiplayerStateManager,
176
254
  anchor = "right",
177
255
  ...props
178
256
  }: {
179
257
  state: S;
180
258
  connectionEvents?: ConnectionEvent<S>[];
259
+ connectedUsers?: MultiplayerUser[];
260
+ tasks?: Task[];
261
+ userId?: string;
181
262
  unstyled?: boolean;
182
263
  colorScheme?: "light" | "dark";
183
- stateManager: StateManager<S, M>;
264
+ multiplayerStateManager: MultiplayerStateManager<S, M>;
184
265
  anchor?: Anchor;
185
266
  } & ComponentPropsWithoutRef<"div">) {
186
267
  const [didMount, setDidMount] = React.useState(false);
@@ -204,11 +285,27 @@ export const StateInspector = memo(function StateInspector<
204
285
  "noya-multiplayer-react-show-history",
205
286
  false
206
287
  );
288
+ const [showUsers, setShowUsers] = useLocalStorageState(
289
+ "noya-multiplayer-react-show-users",
290
+ true
291
+ );
292
+ const [showData, setShowData] = useLocalStorageState(
293
+ "noya-multiplayer-react-show-data",
294
+ true
295
+ );
296
+ const [showTasks, setShowTasks] = useLocalStorageState(
297
+ "noya-multiplayer-react-show-tasks",
298
+ false
299
+ );
207
300
 
208
301
  useEffect(() => {
209
- trackEvents.set(showEvents);
302
+ shouldTrackEvents$.set(showEvents);
210
303
  }, [showEvents]);
211
304
 
305
+ useEffect(() => {
306
+ shouldTrackTasks$.set(showTasks);
307
+ }, [showTasks]);
308
+
212
309
  useEffect(() => {
213
310
  if (eventsContainerRef.current) {
214
311
  eventsContainerRef.current.scrollTop =
@@ -216,7 +313,7 @@ export const StateInspector = memo(function StateInspector<
216
313
  }
217
314
  }, [connectionEvents]);
218
315
 
219
- const historySnapshot = useManagedHistory(stateManager);
316
+ const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
220
317
 
221
318
  useEffect(() => {
222
319
  if (historyContainerRef.current) {
@@ -242,15 +339,15 @@ export const StateInspector = memo(function StateInspector<
242
339
  }, [historySnapshot.historyIndex]);
243
340
 
244
341
  const theme = colorScheme === "light" ? lightTheme : darkTheme;
245
- const borderColor =
246
- colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
342
+ // const borderColor =
343
+ // colorScheme === "light" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.1)";
247
344
  const solidBorderColor =
248
345
  colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
249
346
 
250
347
  const baseStyle: CSSProperties = {
251
348
  position: "fixed",
252
349
  top: 12,
253
- ...(anchor === "right" ? { right: 12 } : { left: 12 }),
350
+ ...(anchor.includes("right") ? { right: 12 } : { left: 12 }),
254
351
  bottom: 12,
255
352
  width: 400,
256
353
  background:
@@ -277,8 +374,10 @@ export const StateInspector = memo(function StateInspector<
277
374
  style={{
278
375
  ...baseStyle,
279
376
  padding: "4px 10px",
280
- bottom: undefined,
281
377
  width: undefined,
378
+ ...(anchor.includes("bottom")
379
+ ? { bottom: 12, top: undefined }
380
+ : { bottom: undefined }),
282
381
  }}
283
382
  onClick={() => setShowInspector(true)}
284
383
  >
@@ -300,12 +399,16 @@ export const StateInspector = memo(function StateInspector<
300
399
  ...props.style,
301
400
  }}
302
401
  >
303
- {/* <div style={{ flex: "0", fontSize: "12px" }}>User ID: {userId}</div> */}
304
402
  <DisclosureSection
305
- open
306
- title="Data"
307
- colorScheme={colorScheme}
308
403
  isFirst
404
+ open={showUsers}
405
+ setOpen={setShowUsers}
406
+ title="Users"
407
+ colorScheme={colorScheme}
408
+ style={{
409
+ flex: "0 0 auto",
410
+ maxHeight: "200px",
411
+ }}
309
412
  right={
310
413
  <ToggleButton
311
414
  showInspector={showInspector}
@@ -315,17 +418,54 @@ export const StateInspector = memo(function StateInspector<
315
418
  />
316
419
  }
317
420
  >
318
- <div
319
- style={{
320
- flex: "1 1 0",
321
- overflowY: "auto",
322
- display: "flex",
323
- flexDirection: "column",
324
- gap: "1px",
325
- padding: "1px 12px",
326
- }}
327
- >
328
- <ObjectInspector data={state} theme={theme} />
421
+ <div style={{ ...styles.sectionInner, flex: "0 0 auto" }}>
422
+ {connectedUsers?.map((user) => (
423
+ <InspectorRow
424
+ key={user.id}
425
+ colorScheme={colorScheme}
426
+ variant={user.id === userId ? "up" : undefined}
427
+ >
428
+ {user.name} ({ellipsis(user.id.toString(), 8, "middle")})
429
+ </InspectorRow>
430
+ ))}
431
+ {!connectedUsers && (
432
+ <div
433
+ style={{
434
+ padding: "12px",
435
+ fontSize: "12px",
436
+ display: "flex",
437
+ flexDirection: "column",
438
+ gap: "4px",
439
+ }}
440
+ >
441
+ <span>No connected users</span>
442
+ </div>
443
+ )}
444
+ </div>
445
+ </DisclosureSection>
446
+ <DisclosureSection
447
+ title="Data"
448
+ colorScheme={colorScheme}
449
+ setOpen={setShowData}
450
+ open={showData}
451
+ >
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
+ )}
329
469
  </div>
330
470
  </DisclosureSection>
331
471
  <DisclosureSection
@@ -334,16 +474,7 @@ export const StateInspector = memo(function StateInspector<
334
474
  title="History"
335
475
  colorScheme={colorScheme}
336
476
  >
337
- <div
338
- ref={historyContainerRef}
339
- style={{
340
- flex: "1 1 0",
341
- overflowY: "auto",
342
- overflowX: "hidden",
343
- display: "flex",
344
- flexDirection: "column",
345
- }}
346
- >
477
+ <div ref={historyContainerRef} style={styles.sectionInner}>
347
478
  <div
348
479
  id={`${HISTORY_ELEMENT_PREFIX}0`}
349
480
  style={{
@@ -427,91 +558,85 @@ export const StateInspector = memo(function StateInspector<
427
558
  </div>
428
559
  </DisclosureSection>
429
560
  <DisclosureSection
430
- open={showEvents}
431
- setOpen={setShowEvents}
432
- title="Events"
561
+ title="Tasks"
433
562
  colorScheme={colorScheme}
563
+ open={showTasks}
564
+ setOpen={setShowTasks}
434
565
  >
435
- <div
436
- ref={eventsContainerRef}
437
- style={{
438
- flex: "1 1 0",
439
- overflowY: "auto",
440
- display: "flex",
441
- flexDirection: "column",
442
- }}
443
- >
444
- {connectionEvents?.map((event, index) => (
445
- <div
446
- key={index}
566
+ <div style={styles.sectionInner}>
567
+ {tasks?.map((task) => (
568
+ <InspectorRow
569
+ key={task.id}
570
+ colorScheme={colorScheme}
447
571
  style={{
448
- borderBottom: `1px solid ${borderColor}`,
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,
449
578
  }}
450
579
  >
451
- {event.type === "stateChange" ? (
452
- <div
453
- style={{
454
- padding: "2px 12px",
455
- background:
456
- colorScheme === "light"
457
- ? "rgba(0,0,0,0.05)"
458
- : "rgba(255,255,255,0.05)",
459
- display: "flex",
460
- alignItems: "center",
461
- }}
462
- >
463
- <span
464
- style={{
465
- fontFamily: "Menlo, monospace",
466
- fontSize: "11px",
467
- borderRadius: 4,
468
- display: "inline-block",
469
- }}
470
- >
471
- connection:{" "}
472
- <span style={{ fontStyle: "italic" }}>
473
- {event.state.toLowerCase()}
474
- </span>
475
- </span>
476
- </div>
477
- ) : (
478
- <div
479
- style={{
480
- padding: "0px 12px 1px",
481
- background:
482
- event.type === "send"
483
- ? "rgba(0,255,0,0.1)"
484
- : "rgba(255,0,0,0.1)",
485
- }}
486
- >
487
- <ObjectInspector
488
- data={event.message}
489
- theme={theme}
490
- nodeRenderer={({
491
- depth,
492
- name,
493
- data,
494
- isNonenumerable,
495
- expanded,
496
- }: any) => {
497
- const direction = event.type === "send" ? "up" : "down";
498
-
499
- return depth === 0 ? (
500
- <ObjectRootLabel direction={direction} data={data} />
501
- ) : (
502
- <ObjectLabel
503
- direction={direction}
504
- name={name}
505
- data={data}
506
- isNonenumerable={isNonenumerable}
507
- />
508
- );
509
- }}
510
- />
511
- </div>
512
- )}
513
- </div>
580
+ <ObjectInspector
581
+ name={task.name}
582
+ data={task.payload}
583
+ theme={theme}
584
+ />
585
+ </InspectorRow>
514
586
  ))}
587
+ </div>
588
+ </DisclosureSection>
589
+ <DisclosureSection
590
+ open={showEvents}
591
+ setOpen={setShowEvents}
592
+ title="Events"
593
+ colorScheme={colorScheme}
594
+ >
595
+ <div ref={eventsContainerRef} style={styles.sectionInner}>
596
+ {connectionEvents?.map((event, index) =>
597
+ event.type === "stateChange" ? (
598
+ <InspectorRow key={index} colorScheme={colorScheme}>
599
+ connection:{" "}
600
+ <span style={{ fontStyle: "italic" }}>
601
+ {event.state.toLowerCase()}
602
+ </span>
603
+ </InspectorRow>
604
+ ) : (
605
+ <InspectorRow
606
+ key={index}
607
+ colorScheme={colorScheme}
608
+ variant={event.type === "send" ? "up" : "down"}
609
+ style={{
610
+ padding: "0px 12px 1px",
611
+ }}
612
+ >
613
+ <ObjectInspector
614
+ data={event.message}
615
+ theme={theme}
616
+ nodeRenderer={({
617
+ depth,
618
+ name,
619
+ data,
620
+ isNonenumerable,
621
+ expanded,
622
+ }: any) => {
623
+ const direction = event.type === "send" ? "up" : "down";
624
+
625
+ return depth === 0 ? (
626
+ <ObjectRootLabel direction={direction} data={data} />
627
+ ) : (
628
+ <ObjectLabel
629
+ direction={direction}
630
+ name={name}
631
+ data={data}
632
+ isNonenumerable={isNonenumerable}
633
+ />
634
+ );
635
+ }}
636
+ />
637
+ </InspectorRow>
638
+ )
639
+ )}
515
640
  {!connectionEvents && (
516
641
  <div
517
642
  style={{
@@ -604,61 +729,6 @@ function Arrow({
604
729
  );
605
730
  }
606
731
 
607
- // <div className="flex py-px flex-col-reverse">
608
- // <button
609
- // type="button"
610
- // className={cx(
611
- // "px-4 py-2 cursor-pointer text-left",
612
- // 0 === historySnapshot.historyIndex && "bg-orange-500/25"
613
- // )}
614
- // onClick={() => {
615
- // // stateManager.goToHistoryIndex(0);
616
- // }}
617
- // >
618
- // <div className="flex justify-between gap-2">
619
- // <span>Initial State</span>
620
- // </div>
621
- // </button>
622
- // {historySnapshot.history.map((entry, index) => (
623
- // <button
624
- // key={index}
625
- // type="button"
626
- // className={cx(
627
- // "px-4 py-2 cursor-pointer text-left",
628
- // index === historySnapshot.historyIndex - 1 &&
629
- // "bg-orange-500/25"
630
- // )}
631
- // onClick={() => {
632
- // // stateManager.goToHistoryIndex(index + 1);
633
- // }}
634
- // >
635
- // <div className="flex justify-between gap-2">
636
- // <span>
637
- // {index + 1}. {entry.metadata.name}
638
- // </span>
639
- // <span>
640
- // {new Date(
641
- // entry.metadata.timestamp
642
- // ).toLocaleTimeString()}
643
- // </span>
644
- // </div>
645
- // <div className="font-mono text-xs">
646
- // {entry.redoPatches?.map((patch, index) => (
647
- // <pre key={index} className="max-w-full">
648
- // {patch.op === "remove"
649
- // ? `Delete ${pathToString(patch.path)}`
650
- // : `${pathToString(patch.path)} = ${JSON.stringify(
651
- // patch.value,
652
- // null,
653
- // 2
654
- // )}`}
655
- // </pre>
656
- // ))}
657
- // </div>
658
- // </button>
659
- // ))}
660
- // </div>
661
-
662
732
  function pathToString(extendedPath: ExtendedPathKey[]) {
663
733
  return extendedPath
664
734
  .map((key) =>
@@ -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,4 +1,11 @@
1
- import { ConnectionEvent, StateManager } from "@noya-app/state-manager";
1
+ "use client";
2
+
3
+ import {
4
+ ConnectionEvent,
5
+ MultiplayerStateManager,
6
+ MultiplayerUser,
7
+ Task,
8
+ } from "@noya-app/state-manager";
2
9
  import React, { ComponentProps, useLayoutEffect } from "react";
3
10
  import { createRoot } from "react-dom/client";
4
11
 
@@ -14,15 +21,21 @@ function createPortalElement() {
14
21
  export function useStateInspector<S extends object, M>({
15
22
  state,
16
23
  connectionEvents,
24
+ connectedUsers,
25
+ tasks,
26
+ userId,
17
27
  disabled = false,
18
28
  colorScheme,
19
29
  anchor,
20
- stateManager,
30
+ multiplayerStateManager,
21
31
  }: {
22
32
  state: S;
23
33
  connectionEvents?: ConnectionEvent<S>[];
34
+ connectedUsers?: MultiplayerUser[];
35
+ tasks?: Task[];
36
+ userId?: string;
24
37
  disabled: boolean;
25
- stateManager: StateManager<S, M>;
38
+ multiplayerStateManager: MultiplayerStateManager<S, M>;
26
39
  } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
27
40
  const [root, setRoot] = React.useState<Root | null>(null);
28
41
 
@@ -49,10 +62,25 @@ export function useStateInspector<S extends object, M>({
49
62
  <StateInspector
50
63
  state={state}
51
64
  connectionEvents={connectionEvents}
65
+ connectedUsers={connectedUsers}
66
+ tasks={tasks}
67
+ userId={userId}
52
68
  colorScheme={colorScheme}
53
69
  anchor={anchor}
54
- stateManager={stateManager as StateManager<any, any>}
70
+ multiplayerStateManager={
71
+ multiplayerStateManager as MultiplayerStateManager<any, any>
72
+ }
55
73
  />
56
74
  );
57
- }, [anchor, colorScheme, connectionEvents, root, state, stateManager]);
75
+ }, [
76
+ anchor,
77
+ colorScheme,
78
+ connectedUsers,
79
+ connectionEvents,
80
+ root,
81
+ state,
82
+ multiplayerStateManager,
83
+ userId,
84
+ tasks,
85
+ ]);
58
86
  }