@noya-app/noya-multiplayer-react 0.1.71 → 0.1.72

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.
@@ -3,8 +3,8 @@
3
3
  import { Observable } from "@noya-app/observable";
4
4
  import { memoGeneric } from "@noya-app/react-utils";
5
5
  import {
6
- EphemeralUserDataManager,
7
6
  MultiplayerUser,
7
+ SharedConnectionDataManager,
8
8
  UserManager,
9
9
  } from "@noya-app/state-manager";
10
10
  import React, { useEffect, useMemo, useState } from "react";
@@ -26,7 +26,7 @@ export type RenderUserPointer = {
26
26
 
27
27
  type InternalUserPointerProps<E extends UserPointerData> = {
28
28
  user: MultiplayerUser;
29
- ephemeralUserDataManager: EphemeralUserDataManager<E>;
29
+ sharedConnectionDataManager: SharedConnectionDataManager<E>;
30
30
  hideAfter?: number;
31
31
  } & RenderUserPointer;
32
32
 
@@ -38,22 +38,22 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
38
38
  E extends UserPointerData,
39
39
  >({
40
40
  user,
41
- ephemeralUserDataManager,
41
+ sharedConnectionDataManager,
42
42
  hideAfter = 5000,
43
43
  renderUserPointer,
44
44
  }: InternalUserPointerProps<E>) {
45
45
  const observable = useMemo(() => {
46
- const metadata$ = ephemeralUserDataManager.metadata$
47
- .observePath([user.id] as const)
46
+ const metadata$ = sharedConnectionDataManager.metadata$
47
+ .observePath([user.connectionId] as const)
48
48
  .throttle(50);
49
- const data$ = ephemeralUserDataManager.data$
50
- .observePath([user.id] as const)
49
+ const data$ = sharedConnectionDataManager.data$
50
+ .observePath([user.connectionId] as const)
51
51
  .throttle(50);
52
52
 
53
53
  return Observable.combine([metadata$, data$], ([metadata, data]) => {
54
54
  return { metadata, data };
55
55
  });
56
- }, [ephemeralUserDataManager, user.id]);
56
+ }, [sharedConnectionDataManager, user.connectionId]);
57
57
 
58
58
  const { metadata, data } = useObservable(observable);
59
59
 
@@ -75,7 +75,7 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
75
75
  if (!data?.pointer) return null;
76
76
 
77
77
  return renderUserPointer({
78
- userId: user.id,
78
+ userId: user.connectionId,
79
79
  name: user.name,
80
80
  point: data.pointer,
81
81
  visible: show,
@@ -84,30 +84,32 @@ const UserPointerInternal = memoGeneric(function UserPointerInternal<
84
84
 
85
85
  export type UserPointersOverlayProps<E extends UserPointerData> = {
86
86
  userManager: UserManager;
87
- ephemeralUserDataManager: EphemeralUserDataManager<E>;
87
+ sharedConnectionDataManager: SharedConnectionDataManager<E>;
88
88
  } & RenderUserPointer;
89
89
 
90
90
  export const UserPointersOverlay = memoGeneric(function UserPointers<
91
91
  E extends UserPointerData,
92
92
  >({
93
93
  userManager,
94
- ephemeralUserDataManager,
94
+ sharedConnectionDataManager,
95
95
  renderUserPointer,
96
96
  }: UserPointersOverlayProps<E>) {
97
- const currentUserId = useObservable(ephemeralUserDataManager.currentUserId$);
97
+ const currentConnectionId = useObservable(
98
+ sharedConnectionDataManager.currentConnectionId$
99
+ );
98
100
  const connectedUsers = useObservable(userManager.connectedUsers$);
99
101
 
100
102
  return (
101
103
  <>
102
104
  {connectedUsers.map((user) => {
103
- if (user.id === currentUserId) return null;
105
+ if (user.connectionId === currentConnectionId) return null;
104
106
 
105
107
  return (
106
108
  <UserPointerInternal
107
- key={user.id}
109
+ key={user.connectionId}
108
110
  user={user}
109
- ephemeralUserDataManager={
110
- ephemeralUserDataManager as unknown as EphemeralUserDataManager<UserPointerData>
111
+ sharedConnectionDataManager={
112
+ sharedConnectionDataManager as unknown as SharedConnectionDataManager<UserPointerData>
111
113
  }
112
114
  renderUserPointer={renderUserPointer}
113
115
  />
package/src/hooks.ts CHANGED
@@ -108,8 +108,9 @@ export type UseMultiplayerStateOptions<
108
108
  M extends object = object,
109
109
  E extends object = object,
110
110
  MenuT extends string = string,
111
+ I extends Record<string, any> = Record<string, unknown>,
111
112
  > = NoyaManagerOptions<S, M> & {
112
- sync?: SyncAdapter<S, M, E, MenuT>;
113
+ sync?: SyncAdapter<S, M, E, MenuT, I>;
113
114
  inspector?: boolean | StateInspectorOptions;
114
115
  };
115
116
 
@@ -119,10 +120,11 @@ export function useBindNoyaManager<
119
120
  M extends object = object,
120
121
  E extends object = object,
121
122
  MenuT extends string = string,
123
+ I extends Record<string, any> = Record<string, unknown>,
122
124
  >(
123
- noyaManager: NoyaManager<S, M, E, MenuT>,
125
+ noyaManager: NoyaManager<S, M, E, MenuT, I>,
124
126
  options?: Pick<
125
- UseMultiplayerStateOptions<S, M, E, MenuT>,
127
+ UseMultiplayerStateOptions<S, M, E, MenuT, I>,
126
128
  "inspector" | "sync"
127
129
  >
128
130
  ) {
@@ -136,7 +138,7 @@ export function useBindNoyaManager<
136
138
  taskManager: typeof noyaManager.taskManager;
137
139
  connectionEventManager: typeof noyaManager.connectionEventManager;
138
140
  userManager: typeof noyaManager.userManager;
139
- ephemeralUserDataManager: typeof noyaManager.ephemeralUserDataManager;
141
+ sharedConnectionDataManager: typeof noyaManager.sharedConnectionDataManager;
140
142
  multiplayerStateManager: typeof noyaManager.multiplayerStateManager;
141
143
  assetManager: typeof noyaManager.assetManager;
142
144
  aiManager: typeof noyaManager.aiManager;
@@ -155,7 +157,7 @@ export function useBindNoyaManager<
155
157
  taskManager: noyaManager.taskManager,
156
158
  connectionEventManager: noyaManager.connectionEventManager,
157
159
  userManager: noyaManager.userManager,
158
- ephemeralUserDataManager: noyaManager.ephemeralUserDataManager,
160
+ sharedConnectionDataManager: noyaManager.sharedConnectionDataManager,
159
161
  multiplayerStateManager: noyaManager.multiplayerStateManager,
160
162
  assetManager: noyaManager.assetManager,
161
163
  aiManager: noyaManager.aiManager,
@@ -179,7 +181,7 @@ export function useBindNoyaManager<
179
181
 
180
182
  useErrorOverlay<MenuT>(noyaManager);
181
183
 
182
- useStateInspector<S, M, E, MenuT>({
184
+ useStateInspector<S, M, E, MenuT, I>({
183
185
  noyaManager,
184
186
  disabled: !options?.inspector,
185
187
  advanced: true,
@@ -194,30 +196,35 @@ export function useMultiplayerState<
194
196
  M extends object = object,
195
197
  E extends object = object,
196
198
  MenuT extends string = string,
199
+ I extends Record<string, any> = Record<string, unknown>,
197
200
  >(
198
201
  initialState: S | (() => S),
199
- options?: UseMultiplayerStateOptions<S, M, E, MenuT>
202
+ options?: UseMultiplayerStateOptions<S, M, E, MenuT, I>
200
203
  ) {
201
204
  const {
202
205
  sync = stubSync<
203
206
  unknown,
204
207
  object,
205
208
  object,
206
- string
207
- >() as unknown as SyncAdapter<S, M, E, MenuT>,
209
+ string,
210
+ Record<string, unknown>
211
+ >() as unknown as SyncAdapter<S, M, E, MenuT, I>,
208
212
  inspector,
209
213
  ...rest
210
214
  } = options ?? {};
211
215
 
212
216
  const [noyaManager] = useState(
213
- () => new NoyaManager<S, M, E, MenuT>(initialState, rest)
217
+ () => new NoyaManager<S, M, E, MenuT, I>(initialState, rest)
214
218
  );
215
219
 
216
- return useBindNoyaManager<S, M, E, MenuT>(noyaManager, { sync, inspector });
220
+ return useBindNoyaManager<S, M, E, MenuT, I>(noyaManager, {
221
+ sync,
222
+ inspector,
223
+ });
217
224
  }
218
225
 
219
226
  function useErrorOverlay<MenuT extends string = string>(
220
- noyaManager: NoyaManager<any, any, any, MenuT>
227
+ noyaManager: NoyaManager<any, any, any, MenuT, any>
221
228
  ) {
222
229
  const unrecoverableError = useObservable(noyaManager.unrecoverableError);
223
230
 
@@ -23,6 +23,7 @@ import {
23
23
  import { ActivityEventsSection } from "./sections/ActivityEventsSection";
24
24
  import { EventsSection } from "./sections/EventsSection";
25
25
  import { HistorySection } from "./sections/HistorySection";
26
+ import { ServerScriptLogsSection } from "./sections/ServerScriptLogsSection";
26
27
  import { exportAll, importAll } from "./serialization";
27
28
  import { StateInspectorButton } from "./StateInspectorButton";
28
29
  import {
@@ -52,6 +53,7 @@ export const StateInspector = memoGeneric(function StateInspector<
52
53
  M extends object,
53
54
  E extends object,
54
55
  MenuT extends string = string,
56
+ I extends Record<string, any> = Record<string, unknown>,
55
57
  >({
56
58
  noyaManager,
57
59
  colorScheme = "light",
@@ -60,7 +62,7 @@ export const StateInspector = memoGeneric(function StateInspector<
60
62
  advanced = false,
61
63
  ...props
62
64
  }: {
63
- noyaManager: NoyaManager<S, M, E, MenuT>;
65
+ noyaManager: NoyaManager<S, M, E, MenuT, I>;
64
66
  unstyled?: boolean;
65
67
  } & StateInspectorOptions &
66
68
  ComponentPropsWithoutRef<"div">) {
@@ -69,12 +71,13 @@ export const StateInspector = memoGeneric(function StateInspector<
69
71
  assetManager,
70
72
  userManager,
71
73
  secretManager,
72
- ephemeralUserDataManager,
74
+ sharedConnectionDataManager,
73
75
  connectionEventManager,
74
76
  taskManager,
75
77
  ioManager,
76
78
  resourceManager,
77
79
  activityEventsManager,
80
+ logManager,
78
81
  } = noyaManager;
79
82
 
80
83
  const [didMount, setDidMount] = React.useState(false);
@@ -88,6 +91,7 @@ export const StateInspector = memoGeneric(function StateInspector<
88
91
 
89
92
  const eventsContainerRef = React.useRef<HTMLDivElement>(null);
90
93
  const activityEventsContainerRef = React.useRef<HTMLDivElement>(null);
94
+ const serverLogsContainerRef = React.useRef<HTMLDivElement>(null);
91
95
 
92
96
  const [showInspector, setShowInspector] = useLocalStorageState(
93
97
  "noya-multiplayer-react-show-inspector",
@@ -113,8 +117,8 @@ export const StateInspector = memoGeneric(function StateInspector<
113
117
  "noya-multiplayer-react-show-tasks",
114
118
  false
115
119
  );
116
- const [showEphemeral, setShowEphemeral] = useLocalStorageState(
117
- "noya-multiplayer-react-show-ephemeral",
120
+ const [showShared, setShowShared] = useLocalStorageState(
121
+ "noya-multiplayer-react-show-shared",
118
122
  false
119
123
  );
120
124
  const [showAssets, setShowAssets] = useLocalStorageState(
@@ -141,6 +145,10 @@ export const StateInspector = memoGeneric(function StateInspector<
141
145
  "noya-multiplayer-react-show-activity-events",
142
146
  false
143
147
  );
148
+ const [showServerLogs, setShowServerLogs] = useLocalStorageState(
149
+ "noya-multiplayer-react-show-server-logs",
150
+ false
151
+ );
144
152
 
145
153
  const connectionEvents = useObservable(connectionEventManager.events$);
146
154
  useEffect(() => {
@@ -149,11 +157,38 @@ export const StateInspector = memoGeneric(function StateInspector<
149
157
  eventsContainerRef.current.scrollHeight;
150
158
  }
151
159
  }, [connectionEvents]);
160
+ const serverLogs = useObservable(logManager.logs$);
161
+ const [serverLogsPaused, setServerLogsPaused] = React.useState(false);
162
+ const [visibleServerLogs, setVisibleServerLogs] = React.useState(
163
+ serverLogs ?? []
164
+ );
165
+
166
+ const toggleServerLogsPaused = useCallback(() => {
167
+ setServerLogsPaused((value) => {
168
+ if (!value) {
169
+ setVisibleServerLogs(serverLogs ?? []);
170
+ }
171
+ return !value;
172
+ });
173
+ }, [serverLogs]);
174
+
175
+ useEffect(() => {
176
+ if (serverLogsPaused) return;
177
+
178
+ setVisibleServerLogs(serverLogs ?? []);
179
+ }, [serverLogs, serverLogsPaused]);
180
+ useEffect(() => {
181
+ if (!showServerLogs) return;
182
+ if (serverLogsContainerRef.current) {
183
+ serverLogsContainerRef.current.scrollTop =
184
+ serverLogsContainerRef.current.scrollHeight;
185
+ }
186
+ }, [showServerLogs, visibleServerLogs]);
152
187
 
153
188
  const multipeerStateInitialized = useObservable(
154
189
  multiplayerStateManager.isInitialized$
155
190
  );
156
- const ephemeral = useObservable(ephemeralUserDataManager.data$);
191
+ const shared = useObservable(sharedConnectionDataManager.data$);
157
192
  const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
158
193
  const assets = useObservable(assetManager.assets$);
159
194
  const resources = useObservable(resourceManager.resources$);
@@ -162,7 +197,7 @@ export const StateInspector = memoGeneric(function StateInspector<
162
197
  const secrets = useObservable(secretManager.secrets$);
163
198
  const secretsInitialized = useObservable(secretManager.isInitialized$);
164
199
  const connectedUsers = useObservable(userManager.connectedUsers$);
165
- const userId = useObservable(userManager.currentUserId$);
200
+ const connectionId = useObservable(userManager.currentConnectionId$);
166
201
  const state = useObservable(multiplayerStateManager.optimisticState$);
167
202
  const inputsInitialized = useObservable(ioManager.inputsInitialized$);
168
203
  const outputTransformsInitialized = useObservable(
@@ -265,32 +300,36 @@ export const StateInspector = memoGeneric(function StateInspector<
265
300
  }
266
301
  >
267
302
  <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
268
- {connectedUsers?.map((user, index, array) => (
269
- <StateInspectorRow
270
- key={user.id}
271
- colorScheme={colorScheme}
272
- variant={user.id === userId ? "up" : undefined}
273
- bordered={index !== array.length - 1}
274
- >
275
- {user.image && (
276
- <img
277
- src={user.image}
278
- alt={user.name}
279
- style={{
280
- width: "13px",
281
- height: "13px",
282
- borderRadius: "50%",
283
- marginRight: "4px",
284
- display: "inline-block",
285
- position: "relative",
286
- background: solidBorderColor,
287
- verticalAlign: "middle",
288
- }}
289
- />
290
- )}
291
- {user.name} ({ellipsis(user.id.toString(), 8, "middle")})
292
- </StateInspectorRow>
293
- ))}
303
+ {connectedUsers?.map((user, index, array) => {
304
+ const identifier = user.userId ?? user.connectionId;
305
+
306
+ return (
307
+ <StateInspectorRow
308
+ key={user.connectionId}
309
+ colorScheme={colorScheme}
310
+ variant={user.connectionId === connectionId ? "up" : undefined}
311
+ bordered={index !== array.length - 1}
312
+ >
313
+ {user.image && (
314
+ <img
315
+ src={user.image}
316
+ alt={user.name}
317
+ style={{
318
+ width: "13px",
319
+ height: "13px",
320
+ borderRadius: "50%",
321
+ marginRight: "4px",
322
+ display: "inline-block",
323
+ position: "relative",
324
+ background: solidBorderColor,
325
+ verticalAlign: "middle",
326
+ }}
327
+ />
328
+ )}
329
+ {user.name} ({ellipsis(identifier, 8, "middle")})
330
+ </StateInspectorRow>
331
+ );
332
+ })}
294
333
  {!connectedUsers && (
295
334
  <div
296
335
  style={{
@@ -686,13 +725,13 @@ export const StateInspector = memoGeneric(function StateInspector<
686
725
  </StateInspectorDisclosureSection>
687
726
  )}
688
727
  <StateInspectorDisclosureSection
689
- open={showEphemeral}
690
- setOpen={setShowEphemeral}
691
- title="Ephemeral User Data"
728
+ open={showShared}
729
+ setOpen={setShowShared}
730
+ title="Shared Connection Data"
692
731
  colorScheme={colorScheme}
693
732
  >
694
733
  <StateInspectorDisclosureRowInner>
695
- {Object.entries(ephemeral).map(([key, value]) => (
734
+ {Object.entries(shared).map(([key, value]) => (
696
735
  <StateInspectorRow key={key} colorScheme={colorScheme}>
697
736
  <ObjectInspector
698
737
  name={key}
@@ -704,6 +743,15 @@ export const StateInspector = memoGeneric(function StateInspector<
704
743
  ))}
705
744
  </StateInspectorDisclosureRowInner>
706
745
  </StateInspectorDisclosureSection>
746
+ <ServerScriptLogsSection
747
+ colorScheme={colorScheme}
748
+ showLogs={showServerLogs}
749
+ setShowLogs={setShowServerLogs}
750
+ logs={visibleServerLogs}
751
+ paused={serverLogsPaused}
752
+ onTogglePaused={toggleServerLogsPaused}
753
+ containerRef={serverLogsContainerRef}
754
+ />
707
755
  {advanced && (
708
756
  <ActivityEventsSection
709
757
  colorScheme={colorScheme}
@@ -0,0 +1,128 @@
1
+ import { ServerScriptLogEntry } from "@noya-app/state-manager";
2
+ import React from "react";
3
+ import { ObjectInspector } from "react-inspector";
4
+ import { getStateInspectorTheme } from "../inspectorTheme";
5
+ import { StateInspectorButton } from "../StateInspectorButton";
6
+ import {
7
+ StateInspectorDisclosureRowInner,
8
+ StateInspectorDisclosureSection,
9
+ } from "../StateInspectorDisclosureSection";
10
+ import { StateInspectorRow } from "../StateInspectorRow";
11
+
12
+ const levelColors: Record<ServerScriptLogEntry["level"], string> = {
13
+ error: "#f87171",
14
+ warn: "#facc15",
15
+ log: "#9ca3af",
16
+ };
17
+
18
+ function formatTimestamp(timestamp: number) {
19
+ return new Date(timestamp).toLocaleTimeString();
20
+ }
21
+
22
+ export function ServerScriptLogsSection({
23
+ showLogs,
24
+ setShowLogs,
25
+ colorScheme,
26
+ logs,
27
+ containerRef,
28
+ paused,
29
+ onTogglePaused,
30
+ }: {
31
+ showLogs: boolean;
32
+ setShowLogs: (value: boolean) => void;
33
+ colorScheme: "light" | "dark";
34
+ logs: ServerScriptLogEntry[];
35
+ containerRef: React.RefObject<HTMLDivElement | null>;
36
+ paused: boolean;
37
+ onTogglePaused: () => void;
38
+ }) {
39
+ const theme = getStateInspectorTheme(colorScheme);
40
+
41
+ return (
42
+ <StateInspectorDisclosureSection
43
+ open={showLogs}
44
+ setOpen={setShowLogs}
45
+ title="Server Script Logs"
46
+ colorScheme={colorScheme}
47
+ right={
48
+ <StateInspectorButton theme={theme} onClick={onTogglePaused}>
49
+ {paused ? "Resume" : "Pause"}
50
+ </StateInspectorButton>
51
+ }
52
+ >
53
+ <StateInspectorDisclosureRowInner ref={containerRef}>
54
+ {paused && (
55
+ <div
56
+ style={{
57
+ padding: "4px 12px",
58
+ fontSize: "11px",
59
+ opacity: 0.7,
60
+ }}
61
+ >
62
+ Log streaming paused
63
+ </div>
64
+ )}
65
+ {logs.map((log) => (
66
+ <StateInspectorRow key={log.id} colorScheme={colorScheme}>
67
+ <div
68
+ style={{
69
+ display: "flex",
70
+ gap: "6px",
71
+ alignItems: "center",
72
+ fontSize: "10px",
73
+ color: "inherit",
74
+ marginBottom: log.values.length ? "4px" : 0,
75
+ textTransform: "uppercase",
76
+ }}
77
+ >
78
+ <span>{formatTimestamp(log.timestamp)}</span>
79
+ <span style={{ color: levelColors[log.level] }}>{log.level}</span>
80
+ <span
81
+ style={{
82
+ fontFamily: "monospace",
83
+ textTransform: "none",
84
+ fontSize: "10px",
85
+ }}
86
+ >
87
+ {log.scriptId}
88
+ </span>
89
+ </div>
90
+ <div
91
+ style={{
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ gap: "4px",
95
+ }}
96
+ >
97
+ {log.values.length > 0 ? (
98
+ log.values.map((value, index) => (
99
+ <ObjectInspector
100
+ key={index}
101
+ data={value}
102
+ theme={theme}
103
+ expandLevel={3}
104
+ />
105
+ ))
106
+ ) : (
107
+ <span style={{ fontSize: "11px", opacity: 0.7 }}>No data</span>
108
+ )}
109
+ </div>
110
+ </StateInspectorRow>
111
+ ))}
112
+ {!logs?.length && (
113
+ <div
114
+ style={{
115
+ padding: "12px",
116
+ fontSize: "12px",
117
+ display: "flex",
118
+ flexDirection: "column",
119
+ gap: "4px",
120
+ }}
121
+ >
122
+ <span>No logs received</span>
123
+ </div>
124
+ )}
125
+ </StateInspectorDisclosureRowInner>
126
+ </StateInspectorDisclosureSection>
127
+ );
128
+ }
@@ -17,7 +17,9 @@ export async function fetchAssetMap(
17
17
  return Object.fromEntries(res);
18
18
  }
19
19
 
20
- export async function exportAll(noyaManager: NoyaManager<any, any, any, any>) {
20
+ export async function exportAll(
21
+ noyaManager: NoyaManager<any, any, any, any, any>
22
+ ) {
21
23
  const { multiplayerStateManager, assetManager } = noyaManager;
22
24
 
23
25
  const assetMap = await fetchAssetMap(assetManager.assets$.get());
@@ -64,7 +66,7 @@ export function encodeAll({
64
66
 
65
67
  export async function importAll(
66
68
  buffer: Uint8Array | ArrayBuffer,
67
- noyaManager: NoyaManager<any, any, any, any>,
69
+ noyaManager: NoyaManager<any, any, any, any, any>,
68
70
  {
69
71
  shouldAllowSchemaChange,
70
72
  }: {
@@ -35,6 +35,7 @@ export function useStateInspector<
35
35
  M extends object,
36
36
  E extends object,
37
37
  MenuT extends string = string,
38
+ I extends Record<string, any> = Record<string, unknown>,
38
39
  >({
39
40
  noyaManager,
40
41
  disabled = false,
@@ -42,7 +43,7 @@ export function useStateInspector<
42
43
  colorScheme,
43
44
  anchor,
44
45
  }: {
45
- noyaManager: NoyaManager<S, M, E, MenuT>;
46
+ noyaManager: NoyaManager<S, M, E, MenuT, I>;
46
47
  disabled?: boolean;
47
48
  advanced?: boolean;
48
49
  colorScheme?: ComponentProps<typeof StateInspector>["colorScheme"];