@noya-app/noya-multiplayer-react 0.1.74 → 0.1.75

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-multiplayer-react",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -11,7 +11,7 @@
11
11
  "dev": "npm run build -- --watch"
12
12
  },
13
13
  "dependencies": {
14
- "@noya-app/state-manager": "0.1.59",
14
+ "@noya-app/state-manager": "0.1.60",
15
15
  "@noya-app/emitter": "0.1.0",
16
16
  "@noya-app/observable": "0.1.12",
17
17
  "@noya-app/task-runner": "0.1.6",
@@ -26,6 +26,7 @@ import {
26
26
  AssetTranscriptionResult,
27
27
  CreateParams,
28
28
  ExtractRequestBody,
29
+ findNoyaUser,
29
30
  MultiplayerPatchMetadata,
30
31
  MultiplayerStateManager,
31
32
  NoyaManager,
@@ -318,39 +319,47 @@ export function usePipelineManager() {
318
319
  return noyaManager.pipelineManager;
319
320
  }
320
321
 
321
- export function useUserManager() {
322
+ export function useNoyaUserManager() {
322
323
  const { noyaManager } = useAnyNoyaStateContext();
323
324
  return noyaManager.userManager;
324
325
  }
325
326
 
326
- export function useConnectedUsers() {
327
- const userManager = useUserManager();
328
- return useObservable(userManager?.connectedUsers$);
327
+ export function useNoyaUsers() {
328
+ const userManager = useNoyaUserManager();
329
+ return useObservable(userManager?.allUsers$);
329
330
  }
330
331
 
331
- export function useConnectedUser(userId?: string | null) {
332
- const userManager = useUserManager();
332
+ export function useNoyaUser(id?: string) {
333
+ const userManager = useNoyaUserManager();
334
+
333
335
  return useObservable(
334
- userManager?.connectedUsers$,
335
- useCallback(
336
- (users) => {
337
- return users.find((user) => {
338
- const prefix = user.connectionId.slice(0, 8);
339
- return userId?.includes(prefix);
340
- });
341
- },
342
- [userId]
343
- )
336
+ userManager?.allUsers$,
337
+ useCallback((users) => findNoyaUser(users, id), [id])
344
338
  );
345
339
  }
346
340
 
341
+ export function useNoyaActiveUsers() {
342
+ const userManager = useNoyaUserManager();
343
+ return useObservable(userManager?.connectedUsers$);
344
+ }
345
+
346
+ export function useNoyaCurrentUser() {
347
+ const userManager = useNoyaUserManager();
348
+ return useObservable(userManager?.currentUser$);
349
+ }
350
+
351
+ export function useNoyaCurrentUserId() {
352
+ const userManager = useNoyaUserManager();
353
+ return useObservable(userManager?.currentId$);
354
+ }
355
+
347
356
  export function useAnySharedConnectionData() {
348
357
  const { noyaManager } = useAnyNoyaStateContext();
349
358
  return noyaManager.sharedConnectionDataManager;
350
359
  }
351
360
 
352
361
  export function useEnqueueInput<
353
- I extends Record<string, any> = Record<string, unknown>
362
+ I extends Record<string, any> = Record<string, unknown>,
354
363
  >() {
355
364
  const { noyaManager } = useAnyNoyaStateContext();
356
365
  return noyaManager.enqueueInput as NoyaManager<
@@ -362,31 +371,6 @@ export function useEnqueueInput<
362
371
  >["enqueueInput"];
363
372
  }
364
373
 
365
- export function useNoyaUser() {
366
- const userManager = useUserManager();
367
- return useObservable(userManager?.currentUser$);
368
- }
369
-
370
- export function useNoyaId() {
371
- const userManager = useUserManager();
372
- return useObservable(userManager?.currentId$);
373
- }
374
-
375
- export function useNoyaUserId() {
376
- const userManager = useUserManager();
377
- return useObservable(userManager?.currentUserId$);
378
- }
379
-
380
- export function useNoyaConnectionId() {
381
- const userManager = useUserManager();
382
- return useObservable(userManager?.currentConnectionId$);
383
- }
384
-
385
- export function useNoyaClientId() {
386
- const userManager = useUserManager();
387
- return useObservable(userManager?.currentClientId$);
388
- }
389
-
390
374
  export function useIsProcessing() {
391
375
  const { noyaManager } = useAnyNoyaStateContext();
392
376
  return useObservable(noyaManager.isProcessing$);
@@ -34,6 +34,7 @@ import { StateInspectorRow } from "./StateInspectorRow";
34
34
  import { StateInspectorTitleLabel } from "./StateInspectorTitleLabel";
35
35
  import { StateInspectorToggleButton } from "./StateInspectorToggleButton";
36
36
  import { useLocalStorageState } from "./useLocalStorageState";
37
+ import { usePausedStream } from "./usePausedStream";
37
38
  import { ellipsis, uploadFile } from "./utils";
38
39
 
39
40
  export type StateInspectorAnchor =
@@ -92,6 +93,7 @@ export const StateInspector = memoGeneric(function StateInspector<
92
93
  const eventsContainerRef = React.useRef<HTMLDivElement>(null);
93
94
  const activityEventsContainerRef = React.useRef<HTMLDivElement>(null);
94
95
  const serverLogsContainerRef = React.useRef<HTMLDivElement>(null);
96
+ const serverSimulationLogsContainerRef = React.useRef<HTMLDivElement>(null);
95
97
 
96
98
  const [showInspector, setShowInspector] = useLocalStorageState(
97
99
  "noya-multiplayer-react-show-inspector",
@@ -153,34 +155,45 @@ export const StateInspector = memoGeneric(function StateInspector<
153
155
  "noya-multiplayer-react-show-server-logs",
154
156
  false
155
157
  );
158
+ const [showServerSimulationLogs, setShowServerSimulationLogs] =
159
+ useLocalStorageState(
160
+ "noya-multiplayer-react-show-server-simulation-logs",
161
+ false
162
+ );
163
+ const [showSimulation, setShowSimulation] = useLocalStorageState(
164
+ "noya-multiplayer-react-show-simulation",
165
+ false
166
+ );
156
167
 
157
168
  const connectionEvents = useObservable(connectionEventManager.events$);
169
+ const {
170
+ visibleItems: visibleConnectionEvents,
171
+ paused: connectionEventsPaused,
172
+ togglePaused: toggleConnectionEventsPaused,
173
+ } = usePausedStream(connectionEvents);
174
+
158
175
  useEffect(() => {
159
176
  if (eventsContainerRef.current) {
160
177
  eventsContainerRef.current.scrollTop =
161
178
  eventsContainerRef.current.scrollHeight;
162
179
  }
163
- }, [connectionEvents]);
180
+ }, [visibleConnectionEvents]);
181
+
164
182
  const serverLogs = useObservable(logManager.logs$);
165
- const [serverLogsPaused, setServerLogsPaused] = React.useState(false);
166
- const [visibleServerLogs, setVisibleServerLogs] = React.useState(
167
- serverLogs ?? []
183
+ const {
184
+ visibleItems: visibleServerLogs,
185
+ paused: serverLogsPaused,
186
+ togglePaused: toggleServerLogsPaused,
187
+ } = usePausedStream(serverLogs);
188
+ const serverSimulationLogs = useObservable(
189
+ multiplayerStateManager.serverSimulationLogs$
168
190
  );
191
+ const {
192
+ visibleItems: visibleServerSimulationLogs,
193
+ paused: serverSimulationLogsPaused,
194
+ togglePaused: toggleServerSimulationLogsPaused,
195
+ } = usePausedStream(serverSimulationLogs);
169
196
 
170
- const toggleServerLogsPaused = useCallback(() => {
171
- setServerLogsPaused((value) => {
172
- if (!value) {
173
- setVisibleServerLogs(serverLogs ?? []);
174
- }
175
- return !value;
176
- });
177
- }, [serverLogs]);
178
-
179
- useEffect(() => {
180
- if (serverLogsPaused) return;
181
-
182
- setVisibleServerLogs(serverLogs ?? []);
183
- }, [serverLogs, serverLogsPaused]);
184
197
  useEffect(() => {
185
198
  if (!showServerLogs) return;
186
199
  if (serverLogsContainerRef.current) {
@@ -188,6 +201,13 @@ export const StateInspector = memoGeneric(function StateInspector<
188
201
  serverLogsContainerRef.current.scrollHeight;
189
202
  }
190
203
  }, [showServerLogs, visibleServerLogs]);
204
+ useEffect(() => {
205
+ if (!showServerSimulationLogs) return;
206
+ if (serverSimulationLogsContainerRef.current) {
207
+ serverSimulationLogsContainerRef.current.scrollTop =
208
+ serverSimulationLogsContainerRef.current.scrollHeight;
209
+ }
210
+ }, [showServerSimulationLogs, visibleServerSimulationLogs]);
191
211
 
192
212
  const multipeerStateInitialized = useObservable(
193
213
  multiplayerStateManager.isInitialized$
@@ -200,8 +220,13 @@ export const StateInspector = memoGeneric(function StateInspector<
200
220
  const assetsInitialized = useObservable(assetManager.isInitialized$);
201
221
  const secrets = useObservable(secretManager.secrets$);
202
222
  const secretsInitialized = useObservable(secretManager.isInitialized$);
203
- const connectedUsers = useObservable(userManager.connectedUsers$);
223
+ const connectedUsers = useObservable(userManager.allUsers$);
204
224
  const connectionId = useObservable(userManager.currentConnectionId$);
225
+ const simulationDiagnostics =
226
+ multiplayerStateManager.getServerSimulationDiagnostics?.() ?? null;
227
+ const showSimulationLogsSection =
228
+ (simulationDiagnostics?.simulateLocally ?? false) ||
229
+ visibleServerSimulationLogs.length > 0;
205
230
  const state = useObservable(multiplayerStateManager.optimisticState$);
206
231
  const inputsInitialized = useObservable(ioManager.inputsInitialized$);
207
232
  const outputTransformsInitialized = useObservable(
@@ -313,38 +338,45 @@ export const StateInspector = memoGeneric(function StateInspector<
313
338
  }
314
339
  >
315
340
  <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
316
- {connectedUsers?.map((user, index, array) => {
317
- return (
318
- <StateInspectorRow
319
- key={user.connectionId}
320
- colorScheme={colorScheme}
321
- variant={user.connectionId === connectionId ? "up" : undefined}
322
- bordered={index !== array.length - 1}
323
- >
324
- {user.image && (
325
- <img
326
- src={user.image}
327
- alt={user.name}
328
- style={{
329
- width: "13px",
330
- height: "13px",
331
- borderRadius: "50%",
332
- marginRight: "4px",
333
- display: "inline-block",
334
- position: "relative",
335
- background: solidBorderColor,
336
- verticalAlign: "middle",
337
- }}
338
- />
339
- )}
340
- {showUserDetails ? (
341
- <ObjectInspector data={user} theme={theme} />
342
- ) : (
343
- user.name
344
- )}
345
- </StateInspectorRow>
346
- );
347
- })}
341
+ {connectedUsers
342
+ ?.filter((user) => showUserDetails || !user.inactive)
343
+ .map((user, index, array) => {
344
+ return (
345
+ <StateInspectorRow
346
+ key={user.connectionId}
347
+ colorScheme={colorScheme}
348
+ variant={
349
+ user.connectionId === connectionId ? "up" : undefined
350
+ }
351
+ bordered={index !== array.length - 1}
352
+ style={{
353
+ opacity: user.inactive ? 0.5 : 1,
354
+ }}
355
+ >
356
+ {user.image && (
357
+ <img
358
+ src={user.image}
359
+ alt={user.name}
360
+ style={{
361
+ width: "13px",
362
+ height: "13px",
363
+ borderRadius: "50%",
364
+ marginRight: "4px",
365
+ display: "inline-block",
366
+ position: "relative",
367
+ background: solidBorderColor,
368
+ verticalAlign: "middle",
369
+ }}
370
+ />
371
+ )}
372
+ {showUserDetails ? (
373
+ <ObjectInspector data={user} theme={theme} />
374
+ ) : (
375
+ user.name
376
+ )}
377
+ </StateInspectorRow>
378
+ );
379
+ })}
348
380
  {!connectedUsers && (
349
381
  <div
350
382
  style={{
@@ -767,6 +799,43 @@ export const StateInspector = memoGeneric(function StateInspector<
767
799
  onTogglePaused={toggleServerLogsPaused}
768
800
  containerRef={serverLogsContainerRef}
769
801
  />
802
+ {showSimulationLogsSection && (
803
+ <ServerScriptLogsSection
804
+ title="Server Script Simulation Logs"
805
+ colorScheme={colorScheme}
806
+ showLogs={showServerSimulationLogs}
807
+ setShowLogs={setShowServerSimulationLogs}
808
+ logs={visibleServerSimulationLogs}
809
+ paused={serverSimulationLogsPaused}
810
+ onTogglePaused={toggleServerSimulationLogsPaused}
811
+ containerRef={serverSimulationLogsContainerRef}
812
+ />
813
+ )}
814
+ <StateInspectorDisclosureSection
815
+ open={showSimulation}
816
+ setOpen={setShowSimulation}
817
+ title="Server Simulation"
818
+ colorScheme={colorScheme}
819
+ >
820
+ <StateInspectorDisclosureRowInner>
821
+ <StateInspectorRow colorScheme={colorScheme}>
822
+ <ObjectInspector
823
+ name="status"
824
+ data={
825
+ simulationDiagnostics ?? {
826
+ simulateLocally: false,
827
+ simulatorActive: false,
828
+ clockSkewMs: 0,
829
+ invocations: [],
830
+ pendingPredictions: [],
831
+ }
832
+ }
833
+ theme={theme}
834
+ expandLevel={2}
835
+ />
836
+ </StateInspectorRow>
837
+ </StateInspectorDisclosureRowInner>
838
+ </StateInspectorDisclosureSection>
770
839
  {advanced && (
771
840
  <ActivityEventsSection
772
841
  colorScheme={colorScheme}
@@ -777,7 +846,9 @@ export const StateInspector = memoGeneric(function StateInspector<
777
846
  />
778
847
  )}
779
848
  <EventsSection<S>
780
- connectionEvents={connectionEvents}
849
+ connectionEvents={visibleConnectionEvents}
850
+ paused={connectionEventsPaused}
851
+ onTogglePaused={toggleConnectionEventsPaused}
781
852
  colorScheme={colorScheme}
782
853
  eventsContainerRef={eventsContainerRef}
783
854
  showEvents={showEvents}
@@ -3,6 +3,7 @@ import React from "react";
3
3
  import { ObjectInspector, ObjectLabel } from "react-inspector";
4
4
  import { getStateInspectorTheme } from "../inspectorTheme";
5
5
  import { ObjectRootLabel } from "../ObjectRootLabel";
6
+ import { StateInspectorButton } from "../StateInspectorButton";
6
7
  import {
7
8
  StateInspectorDisclosureRowInner,
8
9
  StateInspectorDisclosureSection,
@@ -15,12 +16,16 @@ export function EventsSection<S>({
15
16
  colorScheme,
16
17
  eventsContainerRef,
17
18
  connectionEvents,
19
+ paused,
20
+ onTogglePaused,
18
21
  }: {
19
22
  showEvents: boolean;
20
23
  setShowEvents: (value: boolean) => void;
21
24
  colorScheme: "light" | "dark";
22
25
  eventsContainerRef: React.RefObject<HTMLDivElement | null>;
23
26
  connectionEvents: ConnectionEvent<S>[];
27
+ paused: boolean;
28
+ onTogglePaused: () => void;
24
29
  }) {
25
30
  const theme = getStateInspectorTheme(colorScheme);
26
31
 
@@ -30,8 +35,24 @@ export function EventsSection<S>({
30
35
  setOpen={setShowEvents}
31
36
  title="Events"
32
37
  colorScheme={colorScheme}
38
+ right={
39
+ <StateInspectorButton theme={theme} onClick={onTogglePaused}>
40
+ {paused ? "Resume" : "Pause"}
41
+ </StateInspectorButton>
42
+ }
33
43
  >
34
44
  <StateInspectorDisclosureRowInner ref={eventsContainerRef}>
45
+ {paused && (
46
+ <div
47
+ style={{
48
+ padding: "4px 12px",
49
+ fontSize: "11px",
50
+ opacity: 0.7,
51
+ }}
52
+ >
53
+ Event streaming paused
54
+ </div>
55
+ )}
35
56
  {connectionEvents?.map((event, index) =>
36
57
  event.type === "stateChange" ? (
37
58
  <StateInspectorRow key={index} colorScheme={colorScheme}>
@@ -70,7 +91,7 @@ export function EventsSection<S>({
70
91
  </StateInspectorRow>
71
92
  )
72
93
  )}
73
- {!connectionEvents && (
94
+ {!connectionEvents?.length && (
74
95
  <div
75
96
  style={{
76
97
  padding: "12px",
@@ -27,6 +27,7 @@ export function ServerScriptLogsSection({
27
27
  containerRef,
28
28
  paused,
29
29
  onTogglePaused,
30
+ title = "Server Script Logs",
30
31
  }: {
31
32
  showLogs: boolean;
32
33
  setShowLogs: (value: boolean) => void;
@@ -35,6 +36,7 @@ export function ServerScriptLogsSection({
35
36
  containerRef: React.RefObject<HTMLDivElement | null>;
36
37
  paused: boolean;
37
38
  onTogglePaused: () => void;
39
+ title?: string;
38
40
  }) {
39
41
  const theme = getStateInspectorTheme(colorScheme);
40
42
 
@@ -42,7 +44,7 @@ export function ServerScriptLogsSection({
42
44
  <StateInspectorDisclosureSection
43
45
  open={showLogs}
44
46
  setOpen={setShowLogs}
45
- title="Server Script Logs"
47
+ title={title}
46
48
  colorScheme={colorScheme}
47
49
  right={
48
50
  <StateInspectorButton theme={theme} onClick={onTogglePaused}>
@@ -77,6 +79,20 @@ export function ServerScriptLogsSection({
77
79
  >
78
80
  <span>{formatTimestamp(log.timestamp)}</span>
79
81
  <span style={{ color: levelColors[log.level] }}>{log.level}</span>
82
+ {(log.origin || log.target || log.mode) && (
83
+ <span
84
+ style={{
85
+ fontFamily: "monospace",
86
+ textTransform: "none",
87
+ fontSize: "10px",
88
+ opacity: 0.75,
89
+ }}
90
+ >
91
+ {[log.origin ?? log.target, log.mode]
92
+ .filter(Boolean)
93
+ .join(" / ")}
94
+ </span>
95
+ )}
80
96
  <span
81
97
  style={{
82
98
  fontFamily: "monospace",
@@ -100,7 +116,7 @@ export function ServerScriptLogsSection({
100
116
  key={index}
101
117
  data={value}
102
118
  theme={theme}
103
- expandLevel={3}
119
+ // expandLevel={3}
104
120
  />
105
121
  ))
106
122
  ) : (
@@ -0,0 +1,33 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+
5
+ /**
6
+ * Keeps a snapshot of items while paused, resuming updates when unpaused.
7
+ */
8
+ export function usePausedStream<T>(items: T[] | undefined) {
9
+ const [paused, setPaused] = React.useState(false);
10
+ const [visibleItems, setVisibleItems] = React.useState(items ?? []);
11
+
12
+ const togglePaused = React.useCallback(() => {
13
+ setPaused((value) => {
14
+ if (!value) {
15
+ setVisibleItems(items ?? []);
16
+ }
17
+ return !value;
18
+ });
19
+ }, [items]);
20
+
21
+ React.useEffect(() => {
22
+ if (paused) return;
23
+
24
+ setVisibleItems(items ?? []);
25
+ }, [items, paused]);
26
+
27
+ return {
28
+ paused,
29
+ setPaused,
30
+ togglePaused,
31
+ visibleItems,
32
+ };
33
+ }
package/src/singleton.tsx CHANGED
@@ -67,9 +67,8 @@ export function initializeNoyaSingleton<
67
67
  viewType,
68
68
  offlineStorageKey,
69
69
  multiplayerUrl,
70
- serverScripts,
71
70
  ...managerOptions
72
- } = (options ?? {}) as AnyNoyaSingletonOptions<S, M, E, MenuT, I>;
71
+ } = options ?? {};
73
72
 
74
73
  const appData = getAppData<S>(
75
74
  (initialState ?? null) as S | (() => S),
@@ -106,7 +105,7 @@ export function initializeNoyaSingleton<
106
105
  multiplayerUrl: multiplayerUrl ?? appData.multiplayerUrl,
107
106
  offlineStorageKey,
108
107
  debug: managerOptions?.debug,
109
- serverScripts,
108
+ serverScripts: managerOptions?.serverScripts,
110
109
  policyAuthContext,
111
110
  });
112
111