@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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +16 -0
- package/dist/index.bundle.js +28 -28
- package/dist/index.d.mts +10 -9
- package/dist/index.d.ts +10 -9
- package/dist/index.js +323 -212
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +270 -152
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/NoyaStateContext.tsx +26 -42
- package/src/__tests__/noyaApp.test.ts +40 -0
- package/src/inspector/StateInspector.tsx +143 -57
- package/src/inspector/sections/EventsSection.tsx +22 -1
- package/src/inspector/sections/ServerScriptLogsSection.tsx +18 -2
- package/src/inspector/usePausedStream.ts +33 -0
- package/src/noyaApp.ts +28 -0
- package/src/singleton.tsx +2 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-multiplayer-react",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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",
|
package/src/NoyaStateContext.tsx
CHANGED
|
@@ -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
|
|
322
|
+
export function useNoyaUserManager() {
|
|
322
323
|
const { noyaManager } = useAnyNoyaStateContext();
|
|
323
324
|
return noyaManager.userManager;
|
|
324
325
|
}
|
|
325
326
|
|
|
326
|
-
export function
|
|
327
|
-
const userManager =
|
|
328
|
-
return useObservable(userManager?.
|
|
327
|
+
export function useNoyaUsers() {
|
|
328
|
+
const userManager = useNoyaUserManager();
|
|
329
|
+
return useObservable(userManager?.allUsers$);
|
|
329
330
|
}
|
|
330
331
|
|
|
331
|
-
export function
|
|
332
|
-
const userManager =
|
|
332
|
+
export function useNoyaUser(id?: string) {
|
|
333
|
+
const userManager = useNoyaUserManager();
|
|
334
|
+
|
|
333
335
|
return useObservable(
|
|
334
|
-
userManager?.
|
|
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
|
-
}, [
|
|
180
|
+
}, [visibleConnectionEvents]);
|
|
181
|
+
|
|
160
182
|
const serverLogs = useObservable(logManager.logs$);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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: "
|
|
320
|
+
maxHeight: "50%",
|
|
321
|
+
overflowY: "auto",
|
|
292
322
|
}}
|
|
293
323
|
right={
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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={
|
|
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=
|
|
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
|
-
} =
|
|
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
|
|