@noya-app/noya-multiplayer-react 0.1.73 → 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.73",
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.58",
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$);
@@ -1,4 +1,12 @@
1
1
  import { expect, it } from "bun:test";
2
+ import {
3
+ clientId$,
4
+ clientImage$,
5
+ clientName$,
6
+ setClientId,
7
+ setClientImage,
8
+ setClientName,
9
+ } from "@noya-app/state-manager";
2
10
  import { Type } from "..";
3
11
  import { createDefaultAppData, getAppData } from "../noyaApp";
4
12
 
@@ -100,6 +108,38 @@ it("preserves userId from data param", () => {
100
108
  expect(data.isDemo).toBe(true);
101
109
  });
102
110
 
111
+ it("applies client identity from data param", () => {
112
+ setClientId(undefined);
113
+ setClientName(undefined);
114
+ setClientImage(undefined);
115
+
116
+ const existing = {
117
+ theme: "light",
118
+ viewType: "editable",
119
+ initialState: { foo: "bar" },
120
+ inspector: false,
121
+ clientId: "parent-client-id",
122
+ clientName: "Parent Name",
123
+ clientImage: "https://example.com/avatar.svg",
124
+ };
125
+
126
+ getAppData(null as unknown, undefined, {
127
+ window: {
128
+ location: {
129
+ hash: `#data=${encodeURIComponent(JSON.stringify(existing))}`,
130
+ },
131
+ },
132
+ });
133
+
134
+ expect(clientId$.get()).toBe("parent-client-id");
135
+ expect(clientName$.get()).toBe("Parent Name");
136
+ expect(clientImage$.get()).toBe("https://example.com/avatar.svg");
137
+
138
+ setClientId(undefined);
139
+ setClientName(undefined);
140
+ setClientImage(undefined);
141
+ });
142
+
103
143
  // it("enforces schema on data param", () => {
104
144
  // const schema = Type.Object({
105
145
  // foo: Type.String({ default: "bar" }),
@@ -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",
@@ -109,6 +111,10 @@ export const StateInspector = memoGeneric(function StateInspector<
109
111
  "noya-multiplayer-react-show-users",
110
112
  true
111
113
  );
114
+ const [showUserDetails, setShowUserDetails] = useLocalStorageState(
115
+ "noya-multiplayer-react-show-user-details",
116
+ false
117
+ );
112
118
  const [showData, setShowData] = useLocalStorageState(
113
119
  "noya-multiplayer-react-show-data",
114
120
  true
@@ -149,34 +155,45 @@ export const StateInspector = memoGeneric(function StateInspector<
149
155
  "noya-multiplayer-react-show-server-logs",
150
156
  false
151
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
+ );
152
167
 
153
168
  const connectionEvents = useObservable(connectionEventManager.events$);
169
+ const {
170
+ visibleItems: visibleConnectionEvents,
171
+ paused: connectionEventsPaused,
172
+ togglePaused: toggleConnectionEventsPaused,
173
+ } = usePausedStream(connectionEvents);
174
+
154
175
  useEffect(() => {
155
176
  if (eventsContainerRef.current) {
156
177
  eventsContainerRef.current.scrollTop =
157
178
  eventsContainerRef.current.scrollHeight;
158
179
  }
159
- }, [connectionEvents]);
180
+ }, [visibleConnectionEvents]);
181
+
160
182
  const serverLogs = useObservable(logManager.logs$);
161
- const [serverLogsPaused, setServerLogsPaused] = React.useState(false);
162
- const [visibleServerLogs, setVisibleServerLogs] = React.useState(
163
- serverLogs ?? []
183
+ const {
184
+ visibleItems: visibleServerLogs,
185
+ paused: serverLogsPaused,
186
+ togglePaused: toggleServerLogsPaused,
187
+ } = usePausedStream(serverLogs);
188
+ const serverSimulationLogs = useObservable(
189
+ multiplayerStateManager.serverSimulationLogs$
164
190
  );
191
+ const {
192
+ visibleItems: visibleServerSimulationLogs,
193
+ paused: serverSimulationLogsPaused,
194
+ togglePaused: toggleServerSimulationLogsPaused,
195
+ } = usePausedStream(serverSimulationLogs);
165
196
 
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
197
  useEffect(() => {
181
198
  if (!showServerLogs) return;
182
199
  if (serverLogsContainerRef.current) {
@@ -184,6 +201,13 @@ export const StateInspector = memoGeneric(function StateInspector<
184
201
  serverLogsContainerRef.current.scrollHeight;
185
202
  }
186
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]);
187
211
 
188
212
  const multipeerStateInitialized = useObservable(
189
213
  multiplayerStateManager.isInitialized$
@@ -196,8 +220,13 @@ export const StateInspector = memoGeneric(function StateInspector<
196
220
  const assetsInitialized = useObservable(assetManager.isInitialized$);
197
221
  const secrets = useObservable(secretManager.secrets$);
198
222
  const secretsInitialized = useObservable(secretManager.isInitialized$);
199
- const connectedUsers = useObservable(userManager.connectedUsers$);
223
+ const connectedUsers = useObservable(userManager.allUsers$);
200
224
  const connectionId = useObservable(userManager.currentConnectionId$);
225
+ const simulationDiagnostics =
226
+ multiplayerStateManager.getServerSimulationDiagnostics?.() ?? null;
227
+ const showSimulationLogsSection =
228
+ (simulationDiagnostics?.simulateLocally ?? false) ||
229
+ visibleServerSimulationLogs.length > 0;
201
230
  const state = useObservable(multiplayerStateManager.optimisticState$);
202
231
  const inputsInitialized = useObservable(ioManager.inputsInitialized$);
203
232
  const outputTransformsInitialized = useObservable(
@@ -288,48 +317,66 @@ export const StateInspector = memoGeneric(function StateInspector<
288
317
  colorScheme={colorScheme}
289
318
  style={{
290
319
  flex: "0 0 auto",
291
- maxHeight: "200px",
320
+ maxHeight: "50%",
321
+ overflowY: "auto",
292
322
  }}
293
323
  right={
294
- <StateInspectorToggleButton
295
- showInspector={showInspector}
296
- setShowInspector={setShowInspector}
297
- theme={theme}
298
- anchor={anchor}
299
- />
324
+ <span style={{ display: "flex", gap: "10px" }}>
325
+ <StateInspectorButton
326
+ theme={theme}
327
+ onClick={() => setShowUserDetails(!showUserDetails)}
328
+ >
329
+ Toggle details
330
+ </StateInspectorButton>
331
+ <StateInspectorToggleButton
332
+ showInspector={showInspector}
333
+ setShowInspector={setShowInspector}
334
+ theme={theme}
335
+ anchor={anchor}
336
+ />
337
+ </span>
300
338
  }
301
339
  >
302
340
  <StateInspectorDisclosureRowInner style={{ flex: "0 0 auto" }}>
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
- })}
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
+ })}
333
380
  {!connectedUsers && (
334
381
  <div
335
382
  style={{
@@ -752,6 +799,43 @@ export const StateInspector = memoGeneric(function StateInspector<
752
799
  onTogglePaused={toggleServerLogsPaused}
753
800
  containerRef={serverLogsContainerRef}
754
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>
755
839
  {advanced && (
756
840
  <ActivityEventsSection
757
841
  colorScheme={colorScheme}
@@ -762,7 +846,9 @@ export const StateInspector = memoGeneric(function StateInspector<
762
846
  />
763
847
  )}
764
848
  <EventsSection<S>
765
- connectionEvents={connectionEvents}
849
+ connectionEvents={visibleConnectionEvents}
850
+ paused={connectionEventsPaused}
851
+ onTogglePaused={toggleConnectionEventsPaused}
766
852
  colorScheme={colorScheme}
767
853
  eventsContainerRef={eventsContainerRef}
768
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/noyaApp.ts CHANGED
@@ -10,6 +10,9 @@ import {
10
10
  NoyaManager,
11
11
  parentFrameSync,
12
12
  PolicyAuthContext,
13
+ setClientId,
14
+ setClientImage,
15
+ setClientName,
13
16
  Static,
14
17
  stubSync,
15
18
  SyncAdapter,
@@ -34,6 +37,9 @@ export type AppData<State> = {
34
37
  userId?: string;
35
38
  isDemo: boolean;
36
39
  access?: AccessType;
40
+ clientId?: string;
41
+ clientName?: string;
42
+ clientImage?: string;
37
43
  };
38
44
 
39
45
  export function createDefaultAppData<State>(
@@ -84,6 +90,24 @@ export function parseAppDataParameter(
84
90
  }
85
91
  }
86
92
 
93
+ function applyClientIdentityFromAppData(appData: {
94
+ clientId?: string;
95
+ clientName?: string;
96
+ clientImage?: string;
97
+ }) {
98
+ if (appData.clientId !== undefined) {
99
+ setClientId(appData.clientId);
100
+ }
101
+
102
+ if (appData.clientName !== undefined) {
103
+ setClientName(appData.clientName);
104
+ }
105
+
106
+ if (appData.clientImage !== undefined) {
107
+ setClientImage(appData.clientImage);
108
+ }
109
+ }
110
+
87
111
  export function getAppData<State>(
88
112
  initialState: State | (() => State),
89
113
  schema?: TSchema,
@@ -106,6 +130,10 @@ export function getAppData<State>(
106
130
  appData.viewType === "editable" ? "write" : "read";
107
131
  appData.access = appData.access ?? fallbackAccess;
108
132
 
133
+ // If the parent frame provided a client identity (embedded mode), make sure
134
+ // this frame adopts it so the clientId/name/image observables stay in sync.
135
+ applyClientIdentityFromAppData(appData);
136
+
109
137
  return appData as AppData<State>;
110
138
  }
111
139
 
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