@noya-app/noya-multiplayer-react 0.1.12 → 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.
- package/.turbo/turbo-build.log +9 -9
- package/CHANGELOG.md +8 -0
- package/dist/index.d.mts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +99 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +99 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/globals.ts +2 -1
- package/src/hooks.ts +51 -16
- package/src/inspector/StateInspector.tsx +71 -18
- package/src/inspector/useLocalStorageState.tsx +2 -0
- package/src/inspector/useStateInspector.tsx +14 -5
package/src/hooks.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
ActionHandler,
|
|
3
5
|
ConnectionEvent,
|
|
@@ -8,6 +10,7 @@ import {
|
|
|
8
10
|
StateManager,
|
|
9
11
|
StateManagerOptions,
|
|
10
12
|
SyncAdapter,
|
|
13
|
+
Task,
|
|
11
14
|
stubSync,
|
|
12
15
|
} from "@noya-app/state-manager";
|
|
13
16
|
import {
|
|
@@ -20,7 +23,11 @@ import {
|
|
|
20
23
|
useState,
|
|
21
24
|
useSyncExternalStore,
|
|
22
25
|
} from "react";
|
|
23
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
shouldTrackEvents$,
|
|
28
|
+
shouldTrackTasks$,
|
|
29
|
+
useObservable,
|
|
30
|
+
} from "./globals";
|
|
24
31
|
import { StateInspector } from "./inspector/StateInspector";
|
|
25
32
|
import { useStateInspector } from "./inspector/useStateInspector";
|
|
26
33
|
|
|
@@ -134,21 +141,21 @@ const defaultStubSync = stubSync();
|
|
|
134
141
|
|
|
135
142
|
export function useMultiplayerState<S extends object, M = void>(
|
|
136
143
|
initialState: S | (() => S),
|
|
137
|
-
options
|
|
144
|
+
options?: MultiplayerStateManagerOptions<S, M> & {
|
|
138
145
|
sync?: (options: { ms: MultiplayerStateManager<S, M> }) => void;
|
|
139
146
|
trackConnectionEvents?: boolean;
|
|
147
|
+
trackTasks?: boolean;
|
|
140
148
|
inspector?:
|
|
141
149
|
| boolean
|
|
142
150
|
| Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">;
|
|
143
151
|
}
|
|
144
152
|
) {
|
|
145
153
|
const { sync = defaultStubSync as unknown as SyncAdapter<S, M>, ...rest } =
|
|
146
|
-
options;
|
|
154
|
+
options ?? {};
|
|
147
155
|
|
|
148
156
|
const [multiplayerStateManager] = useState(
|
|
149
157
|
() => new MultiplayerStateManager<S, M>(initialState, rest)
|
|
150
158
|
);
|
|
151
|
-
|
|
152
159
|
const syncRef = useRef(sync);
|
|
153
160
|
|
|
154
161
|
const [connectionEvents, setConnectionEvents] = useState<
|
|
@@ -156,43 +163,69 @@ export function useMultiplayerState<S extends object, M = void>(
|
|
|
156
163
|
>();
|
|
157
164
|
|
|
158
165
|
const [connectedUsers, setConnectedUsers] = useState<MultiplayerUser[]>([]);
|
|
166
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
159
167
|
const [userId, setUserId] = useState<string | undefined>();
|
|
160
168
|
|
|
161
169
|
const extras = useMemo(() => {
|
|
162
170
|
return {
|
|
171
|
+
tasks,
|
|
163
172
|
connectionEvents,
|
|
164
173
|
connectedUsers,
|
|
174
|
+
// evaluate: async (code: string) => {
|
|
175
|
+
// const payload = await rpcManager.request({ type: "eval", code });
|
|
176
|
+
|
|
177
|
+
// if (payload.type !== "eval") {
|
|
178
|
+
// throw new Error(`Unexpected rpc response type: ${payload.type}`);
|
|
179
|
+
// }
|
|
180
|
+
// },
|
|
165
181
|
};
|
|
166
|
-
}, [connectionEvents, connectedUsers]);
|
|
182
|
+
}, [tasks, connectionEvents, connectedUsers]);
|
|
167
183
|
|
|
168
|
-
const globalTrackEvents = useObservable(
|
|
169
|
-
const
|
|
170
|
-
|
|
184
|
+
const globalTrackEvents = useObservable(shouldTrackEvents$);
|
|
185
|
+
const globalTrackTasks = useObservable(shouldTrackTasks$);
|
|
186
|
+
|
|
187
|
+
const shouldTrackEvents = options?.trackConnectionEvents ?? globalTrackEvents;
|
|
188
|
+
const shouldTrackTasks = options?.trackTasks ?? globalTrackTasks;
|
|
171
189
|
|
|
172
190
|
const trackEventsCallbackRef = useRef<
|
|
173
191
|
((e: ConnectionEvent<S>) => void) | undefined
|
|
174
192
|
>();
|
|
175
|
-
|
|
176
193
|
const trackConnectedUsersCallbackRef = useRef<
|
|
177
194
|
((users: MultiplayerUser[], userId: string) => void) | undefined
|
|
178
195
|
>();
|
|
196
|
+
const trackTasksCallbackRef = useRef<((tasks: Task[]) => void) | undefined>();
|
|
179
197
|
|
|
180
198
|
useEffect(() => {
|
|
181
|
-
trackEventsCallbackRef.current =
|
|
199
|
+
trackEventsCallbackRef.current = shouldTrackEvents
|
|
182
200
|
? (e) =>
|
|
183
201
|
setConnectionEvents((events) => {
|
|
184
202
|
const updated = events ? [...events, e] : [e];
|
|
185
203
|
return updated.length > 50 ? updated.slice(-50) : updated;
|
|
186
204
|
})
|
|
187
205
|
: undefined;
|
|
188
|
-
}, [
|
|
206
|
+
}, [shouldTrackEvents]);
|
|
189
207
|
|
|
190
208
|
useEffect(() => {
|
|
191
209
|
trackConnectedUsersCallbackRef.current = (users, userId) => {
|
|
192
210
|
setConnectedUsers(users);
|
|
193
211
|
setUserId(userId);
|
|
194
212
|
};
|
|
195
|
-
});
|
|
213
|
+
}, []);
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
trackTasksCallbackRef.current = shouldTrackTasks
|
|
217
|
+
? (tasks) => {
|
|
218
|
+
setTasks((old) => {
|
|
219
|
+
const newIds = new Set(tasks.map((t) => t.id));
|
|
220
|
+
|
|
221
|
+
// merge old and new tasks, preferring new tasks
|
|
222
|
+
const unchanged = old.filter((t) => !newIds.has(t.id));
|
|
223
|
+
|
|
224
|
+
return [...unchanged, ...tasks];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
: undefined;
|
|
228
|
+
}, [shouldTrackTasks]);
|
|
196
229
|
|
|
197
230
|
useEffect(() => {
|
|
198
231
|
if (syncRef.current) {
|
|
@@ -201,6 +234,7 @@ export function useMultiplayerState<S extends object, M = void>(
|
|
|
201
234
|
onConnectionEvent: (e) => trackEventsCallbackRef.current?.(e),
|
|
202
235
|
onChangeConnectedUsers: (users, userId) =>
|
|
203
236
|
trackConnectedUsersCallbackRef.current?.(users, userId),
|
|
237
|
+
onChangeTasks: (tasks) => trackTasksCallbackRef.current?.(tasks),
|
|
204
238
|
});
|
|
205
239
|
}
|
|
206
240
|
}, [multiplayerStateManager]);
|
|
@@ -208,18 +242,19 @@ export function useMultiplayerState<S extends object, M = void>(
|
|
|
208
242
|
const state = useSyncMultiplayerStateManager(multiplayerStateManager);
|
|
209
243
|
|
|
210
244
|
useStateInspector({
|
|
211
|
-
|
|
245
|
+
multiplayerStateManager,
|
|
212
246
|
state,
|
|
213
247
|
connectionEvents,
|
|
214
248
|
connectedUsers,
|
|
249
|
+
tasks,
|
|
215
250
|
userId,
|
|
216
|
-
disabled: !options
|
|
217
|
-
...(typeof options
|
|
251
|
+
disabled: !options?.inspector,
|
|
252
|
+
...(typeof options?.inspector === "object" && options.inspector),
|
|
218
253
|
});
|
|
219
254
|
|
|
220
255
|
return [
|
|
221
256
|
state,
|
|
222
|
-
multiplayerStateManager.
|
|
257
|
+
multiplayerStateManager.setState,
|
|
223
258
|
multiplayerStateManager,
|
|
224
259
|
extras,
|
|
225
260
|
] as const;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
ConnectionEvent,
|
|
3
5
|
ExtendedPathKey,
|
|
6
|
+
MultiplayerStateManager,
|
|
4
7
|
MultiplayerUser,
|
|
5
|
-
|
|
8
|
+
Task,
|
|
6
9
|
} from "@noya-app/state-manager";
|
|
7
10
|
import React, {
|
|
8
11
|
CSSProperties,
|
|
@@ -20,7 +23,7 @@ import {
|
|
|
20
23
|
chromeDark,
|
|
21
24
|
chromeLight,
|
|
22
25
|
} from "react-inspector";
|
|
23
|
-
import {
|
|
26
|
+
import { shouldTrackEvents$, shouldTrackTasks$ } from "../globals";
|
|
24
27
|
import { useManagedHistory } from "../hooks";
|
|
25
28
|
import { useLocalStorageState } from "./useLocalStorageState";
|
|
26
29
|
|
|
@@ -170,7 +173,7 @@ function DisclosureSection({
|
|
|
170
173
|
}}
|
|
171
174
|
/>
|
|
172
175
|
)}
|
|
173
|
-
<span style={{ flex: "1 1 0" }}>{title}</span>
|
|
176
|
+
<span style={{ flex: "1 1 0", userSelect: "none" }}>{title}</span>
|
|
174
177
|
{right}
|
|
175
178
|
</div>
|
|
176
179
|
{open && children}
|
|
@@ -234,7 +237,7 @@ function InspectorRow({
|
|
|
234
237
|
|
|
235
238
|
const HISTORY_ELEMENT_PREFIX = "noya-multiplayer-history-";
|
|
236
239
|
|
|
237
|
-
type Anchor = "left" | "right";
|
|
240
|
+
type Anchor = "left" | "right" | "bottom left" | "bottom right";
|
|
238
241
|
|
|
239
242
|
export const StateInspector = memo(function StateInspector<
|
|
240
243
|
S extends object,
|
|
@@ -243,20 +246,22 @@ export const StateInspector = memo(function StateInspector<
|
|
|
243
246
|
state,
|
|
244
247
|
connectionEvents,
|
|
245
248
|
connectedUsers,
|
|
249
|
+
tasks,
|
|
246
250
|
userId,
|
|
247
251
|
unstyled,
|
|
248
252
|
colorScheme = "light",
|
|
249
|
-
|
|
253
|
+
multiplayerStateManager,
|
|
250
254
|
anchor = "right",
|
|
251
255
|
...props
|
|
252
256
|
}: {
|
|
253
257
|
state: S;
|
|
254
258
|
connectionEvents?: ConnectionEvent<S>[];
|
|
255
259
|
connectedUsers?: MultiplayerUser[];
|
|
260
|
+
tasks?: Task[];
|
|
256
261
|
userId?: string;
|
|
257
262
|
unstyled?: boolean;
|
|
258
263
|
colorScheme?: "light" | "dark";
|
|
259
|
-
|
|
264
|
+
multiplayerStateManager: MultiplayerStateManager<S, M>;
|
|
260
265
|
anchor?: Anchor;
|
|
261
266
|
} & ComponentPropsWithoutRef<"div">) {
|
|
262
267
|
const [didMount, setDidMount] = React.useState(false);
|
|
@@ -288,11 +293,19 @@ export const StateInspector = memo(function StateInspector<
|
|
|
288
293
|
"noya-multiplayer-react-show-data",
|
|
289
294
|
true
|
|
290
295
|
);
|
|
296
|
+
const [showTasks, setShowTasks] = useLocalStorageState(
|
|
297
|
+
"noya-multiplayer-react-show-tasks",
|
|
298
|
+
false
|
|
299
|
+
);
|
|
291
300
|
|
|
292
301
|
useEffect(() => {
|
|
293
|
-
|
|
302
|
+
shouldTrackEvents$.set(showEvents);
|
|
294
303
|
}, [showEvents]);
|
|
295
304
|
|
|
305
|
+
useEffect(() => {
|
|
306
|
+
shouldTrackTasks$.set(showTasks);
|
|
307
|
+
}, [showTasks]);
|
|
308
|
+
|
|
296
309
|
useEffect(() => {
|
|
297
310
|
if (eventsContainerRef.current) {
|
|
298
311
|
eventsContainerRef.current.scrollTop =
|
|
@@ -300,7 +313,7 @@ export const StateInspector = memo(function StateInspector<
|
|
|
300
313
|
}
|
|
301
314
|
}, [connectionEvents]);
|
|
302
315
|
|
|
303
|
-
const historySnapshot = useManagedHistory(
|
|
316
|
+
const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
|
|
304
317
|
|
|
305
318
|
useEffect(() => {
|
|
306
319
|
if (historyContainerRef.current) {
|
|
@@ -334,7 +347,7 @@ export const StateInspector = memo(function StateInspector<
|
|
|
334
347
|
const baseStyle: CSSProperties = {
|
|
335
348
|
position: "fixed",
|
|
336
349
|
top: 12,
|
|
337
|
-
...(anchor
|
|
350
|
+
...(anchor.includes("right") ? { right: 12 } : { left: 12 }),
|
|
338
351
|
bottom: 12,
|
|
339
352
|
width: 400,
|
|
340
353
|
background:
|
|
@@ -361,8 +374,10 @@ export const StateInspector = memo(function StateInspector<
|
|
|
361
374
|
style={{
|
|
362
375
|
...baseStyle,
|
|
363
376
|
padding: "4px 10px",
|
|
364
|
-
bottom: undefined,
|
|
365
377
|
width: undefined,
|
|
378
|
+
...(anchor.includes("bottom")
|
|
379
|
+
? { bottom: 12, top: undefined }
|
|
380
|
+
: { bottom: undefined }),
|
|
366
381
|
}}
|
|
367
382
|
onClick={() => setShowInspector(true)}
|
|
368
383
|
>
|
|
@@ -434,14 +449,23 @@ export const StateInspector = memo(function StateInspector<
|
|
|
434
449
|
setOpen={setShowData}
|
|
435
450
|
open={showData}
|
|
436
451
|
>
|
|
437
|
-
<div
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
+
)}
|
|
445
469
|
</div>
|
|
446
470
|
</DisclosureSection>
|
|
447
471
|
<DisclosureSection
|
|
@@ -533,6 +557,35 @@ export const StateInspector = memo(function StateInspector<
|
|
|
533
557
|
))}
|
|
534
558
|
</div>
|
|
535
559
|
</DisclosureSection>
|
|
560
|
+
<DisclosureSection
|
|
561
|
+
title="Tasks"
|
|
562
|
+
colorScheme={colorScheme}
|
|
563
|
+
open={showTasks}
|
|
564
|
+
setOpen={setShowTasks}
|
|
565
|
+
>
|
|
566
|
+
<div style={styles.sectionInner}>
|
|
567
|
+
{tasks?.map((task) => (
|
|
568
|
+
<InspectorRow
|
|
569
|
+
key={task.id}
|
|
570
|
+
colorScheme={colorScheme}
|
|
571
|
+
style={{
|
|
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,
|
|
578
|
+
}}
|
|
579
|
+
>
|
|
580
|
+
<ObjectInspector
|
|
581
|
+
name={task.name}
|
|
582
|
+
data={task.payload}
|
|
583
|
+
theme={theme}
|
|
584
|
+
/>
|
|
585
|
+
</InspectorRow>
|
|
586
|
+
))}
|
|
587
|
+
</div>
|
|
588
|
+
</DisclosureSection>
|
|
536
589
|
<DisclosureSection
|
|
537
590
|
open={showEvents}
|
|
538
591
|
setOpen={setShowEvents}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
ConnectionEvent,
|
|
5
|
+
MultiplayerStateManager,
|
|
3
6
|
MultiplayerUser,
|
|
4
|
-
|
|
7
|
+
Task,
|
|
5
8
|
} from "@noya-app/state-manager";
|
|
6
9
|
import React, { ComponentProps, useLayoutEffect } from "react";
|
|
7
10
|
import { createRoot } from "react-dom/client";
|
|
@@ -19,18 +22,20 @@ export function useStateInspector<S extends object, M>({
|
|
|
19
22
|
state,
|
|
20
23
|
connectionEvents,
|
|
21
24
|
connectedUsers,
|
|
25
|
+
tasks,
|
|
22
26
|
userId,
|
|
23
27
|
disabled = false,
|
|
24
28
|
colorScheme,
|
|
25
29
|
anchor,
|
|
26
|
-
|
|
30
|
+
multiplayerStateManager,
|
|
27
31
|
}: {
|
|
28
32
|
state: S;
|
|
29
33
|
connectionEvents?: ConnectionEvent<S>[];
|
|
30
34
|
connectedUsers?: MultiplayerUser[];
|
|
35
|
+
tasks?: Task[];
|
|
31
36
|
userId?: string;
|
|
32
37
|
disabled: boolean;
|
|
33
|
-
|
|
38
|
+
multiplayerStateManager: MultiplayerStateManager<S, M>;
|
|
34
39
|
} & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
|
|
35
40
|
const [root, setRoot] = React.useState<Root | null>(null);
|
|
36
41
|
|
|
@@ -58,10 +63,13 @@ export function useStateInspector<S extends object, M>({
|
|
|
58
63
|
state={state}
|
|
59
64
|
connectionEvents={connectionEvents}
|
|
60
65
|
connectedUsers={connectedUsers}
|
|
66
|
+
tasks={tasks}
|
|
61
67
|
userId={userId}
|
|
62
68
|
colorScheme={colorScheme}
|
|
63
69
|
anchor={anchor}
|
|
64
|
-
|
|
70
|
+
multiplayerStateManager={
|
|
71
|
+
multiplayerStateManager as MultiplayerStateManager<any, any>
|
|
72
|
+
}
|
|
65
73
|
/>
|
|
66
74
|
);
|
|
67
75
|
}, [
|
|
@@ -71,7 +79,8 @@ export function useStateInspector<S extends object, M>({
|
|
|
71
79
|
connectionEvents,
|
|
72
80
|
root,
|
|
73
81
|
state,
|
|
74
|
-
|
|
82
|
+
multiplayerStateManager,
|
|
75
83
|
userId,
|
|
84
|
+
tasks,
|
|
76
85
|
]);
|
|
77
86
|
}
|