@noya-app/noya-multiplayer-react 0.1.45 → 0.1.46
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 +10 -0
- package/dist/index.bundle.js +10 -10
- package/dist/index.d.mts +81 -86
- package/dist/index.d.ts +81 -86
- package/dist/index.js +371 -418
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -312
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/NoyaStateContext.tsx +96 -95
- package/src/WebSocketConnection.ts +153 -0
- package/src/components/ErrorOverlay.tsx +3 -3
- package/src/hooks.ts +74 -191
- package/src/index.ts +2 -0
- package/src/inspector/StateInspector.tsx +83 -45
- package/src/inspector/useStateInspector.tsx +11 -55
- package/src/noyaApp.ts +36 -95
- package/src/globals.ts +0 -4
package/src/NoyaStateContext.tsx
CHANGED
|
@@ -5,43 +5,41 @@ import {
|
|
|
5
5
|
PathKey,
|
|
6
6
|
} from "@noya-app/observable";
|
|
7
7
|
import {
|
|
8
|
-
AIManager,
|
|
9
|
-
AssetManager,
|
|
10
8
|
ConnectedUsersManager,
|
|
11
9
|
EphemeralUserDataManager,
|
|
12
10
|
MultiplayerStateManager,
|
|
13
|
-
|
|
11
|
+
NoyaManager,
|
|
14
12
|
StateManagerOptions,
|
|
15
13
|
Static,
|
|
16
14
|
TSchema,
|
|
17
|
-
WorkflowManager,
|
|
18
15
|
} from "@noya-app/state-manager";
|
|
19
16
|
import React, {
|
|
20
17
|
createContext,
|
|
21
18
|
SetStateAction,
|
|
22
19
|
useCallback,
|
|
23
20
|
useContext,
|
|
21
|
+
useEffect,
|
|
24
22
|
useMemo,
|
|
25
23
|
} from "react";
|
|
26
24
|
import { useNoyaState, UseNoyaStateOptions } from "./noyaApp";
|
|
27
25
|
import { useObservable } from "./useObservable";
|
|
28
26
|
|
|
29
|
-
interface NoyaStateProviderProps<
|
|
30
|
-
|
|
27
|
+
interface NoyaStateProviderProps<
|
|
28
|
+
S,
|
|
29
|
+
M extends object,
|
|
30
|
+
E extends object,
|
|
31
|
+
MenuT extends string,
|
|
32
|
+
> extends Omit<
|
|
33
|
+
UseNoyaStateOptions<S, M, E, MenuT>,
|
|
34
|
+
"schema" | "mergeHistoryEntries"
|
|
35
|
+
> {
|
|
31
36
|
children: React.ReactNode;
|
|
32
37
|
initialState?: S;
|
|
33
38
|
fallback?: React.ReactNode;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
const AnyNoyaStateContext = createContext<
|
|
37
|
-
|
|
|
38
|
-
ms: MultiplayerStateManager<any, any>;
|
|
39
|
-
assetManager: AssetManager;
|
|
40
|
-
workflowManager: WorkflowManager;
|
|
41
|
-
secretManager: SecretManager;
|
|
42
|
-
aiManager: AIManager;
|
|
43
|
-
}
|
|
44
|
-
| undefined
|
|
42
|
+
NoyaManager<any, any, any, any> | undefined
|
|
45
43
|
>(undefined);
|
|
46
44
|
|
|
47
45
|
function useAnyNoyaStateContext() {
|
|
@@ -106,7 +104,12 @@ export function useSecret(name: string) {
|
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
export function useIsInitialized() {
|
|
109
|
-
const {
|
|
107
|
+
const {
|
|
108
|
+
multiplayerStateManager: ms,
|
|
109
|
+
secretManager,
|
|
110
|
+
assetManager,
|
|
111
|
+
} = useAnyNoyaStateContext();
|
|
112
|
+
|
|
110
113
|
const isInitializedObservable = useMemo(
|
|
111
114
|
() =>
|
|
112
115
|
Observable.combine(
|
|
@@ -163,8 +166,9 @@ export const FallbackUntilInitialized = ({
|
|
|
163
166
|
|
|
164
167
|
export function createNoyaContext<
|
|
165
168
|
Schema extends TSchema,
|
|
166
|
-
M =
|
|
169
|
+
M extends object = object,
|
|
167
170
|
E extends object = object,
|
|
171
|
+
MenuT extends string = string,
|
|
168
172
|
>({
|
|
169
173
|
schema,
|
|
170
174
|
mergeHistoryEntries,
|
|
@@ -178,12 +182,7 @@ export function createNoyaContext<
|
|
|
178
182
|
type S = Static<Schema>;
|
|
179
183
|
|
|
180
184
|
const NoyaStateContext = AnyNoyaStateContext as React.Context<
|
|
181
|
-
|
|
|
182
|
-
ms: MultiplayerStateManager<S, M>;
|
|
183
|
-
assetManager: AssetManager;
|
|
184
|
-
secretManager: SecretManager;
|
|
185
|
-
}
|
|
186
|
-
| undefined
|
|
185
|
+
NoyaManager<S, M, E> | undefined
|
|
187
186
|
>;
|
|
188
187
|
|
|
189
188
|
const EphemeralUserDataManagerContext =
|
|
@@ -195,47 +194,20 @@ export function createNoyaContext<
|
|
|
195
194
|
children,
|
|
196
195
|
initialState,
|
|
197
196
|
...options
|
|
198
|
-
}: NoyaStateProviderProps<S, M, E>) {
|
|
199
|
-
const [
|
|
200
|
-
,
|
|
201
|
-
,
|
|
202
|
-
{
|
|
203
|
-
multiplayerStateManager,
|
|
204
|
-
assetManager,
|
|
205
|
-
workflowManager,
|
|
206
|
-
secretManager,
|
|
207
|
-
aiManager,
|
|
208
|
-
ephemeralUserDataManager,
|
|
209
|
-
connectedUsersManager,
|
|
210
|
-
},
|
|
211
|
-
] = useNoyaState<S, M, E>(initialState, {
|
|
197
|
+
}: NoyaStateProviderProps<S, M, E, MenuT>) {
|
|
198
|
+
const [, , { noyaManager }] = useNoyaState<S, M, E, MenuT>(initialState, {
|
|
212
199
|
...(options as any),
|
|
213
200
|
schema,
|
|
214
201
|
mergeHistoryEntries,
|
|
215
202
|
});
|
|
216
203
|
|
|
217
|
-
const value = useMemo(
|
|
218
|
-
() => ({
|
|
219
|
-
ms: multiplayerStateManager as MultiplayerStateManager<any, any>,
|
|
220
|
-
assetManager,
|
|
221
|
-
workflowManager,
|
|
222
|
-
secretManager,
|
|
223
|
-
aiManager,
|
|
224
|
-
}),
|
|
225
|
-
[
|
|
226
|
-
multiplayerStateManager,
|
|
227
|
-
assetManager,
|
|
228
|
-
workflowManager,
|
|
229
|
-
secretManager,
|
|
230
|
-
aiManager,
|
|
231
|
-
]
|
|
232
|
-
);
|
|
233
|
-
|
|
234
204
|
return (
|
|
235
|
-
<AnyNoyaStateContext.Provider value={
|
|
236
|
-
<ConnectedUsersContext.Provider
|
|
205
|
+
<AnyNoyaStateContext.Provider value={noyaManager}>
|
|
206
|
+
<ConnectedUsersContext.Provider
|
|
207
|
+
value={noyaManager.connectedUsersManager}
|
|
208
|
+
>
|
|
237
209
|
<EphemeralUserDataManagerContext.Provider
|
|
238
|
-
value={ephemeralUserDataManager}
|
|
210
|
+
value={noyaManager.ephemeralUserDataManager}
|
|
239
211
|
>
|
|
240
212
|
{options.fallback !== undefined ? (
|
|
241
213
|
<FallbackUntilInitialized fallback={options.fallback}>
|
|
@@ -260,7 +232,7 @@ export function createNoyaContext<
|
|
|
260
232
|
path?: PathKey[] | string | ((value: S) => any),
|
|
261
233
|
options?: Pick<ObservableOptions, "isEqual">
|
|
262
234
|
) {
|
|
263
|
-
const { ms } = useAnyNoyaStateContext();
|
|
235
|
+
const { multiplayerStateManager: ms } = useAnyNoyaStateContext();
|
|
264
236
|
|
|
265
237
|
let actualPath: PathKey[] | string =
|
|
266
238
|
typeof path === "function" || !path ? "" : path;
|
|
@@ -277,64 +249,65 @@ export function createNoyaContext<
|
|
|
277
249
|
return value;
|
|
278
250
|
}
|
|
279
251
|
|
|
280
|
-
function useSetValue():
|
|
281
|
-
|
|
282
|
-
|
|
252
|
+
function useSetValue(): (
|
|
253
|
+
...args:
|
|
254
|
+
| [metadata: M, value: SetStateAction<S>]
|
|
255
|
+
| [value: SetStateAction<S>]
|
|
256
|
+
) => void;
|
|
283
257
|
function useSetValue<P extends PathKey[] | string>(
|
|
284
258
|
path: P
|
|
285
|
-
):
|
|
286
|
-
|
|
287
|
-
|
|
259
|
+
): (
|
|
260
|
+
...args:
|
|
261
|
+
| [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
|
|
262
|
+
| [value: SetStateAction<GetAtPath<S, P>>]
|
|
263
|
+
) => void;
|
|
288
264
|
function useSetValue<P extends PathKey[] | string>(path?: P) {
|
|
289
|
-
const { ms } = useAnyNoyaStateContext();
|
|
265
|
+
const { multiplayerStateManager: ms } = useAnyNoyaStateContext();
|
|
290
266
|
|
|
291
267
|
const setValue = useCallback(
|
|
292
268
|
(
|
|
293
|
-
...args:
|
|
294
|
-
|
|
295
|
-
|
|
269
|
+
...args:
|
|
270
|
+
| [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
|
|
271
|
+
| [value: SetStateAction<GetAtPath<S, P>>]
|
|
296
272
|
): void => {
|
|
297
|
-
const [metadata, value] =
|
|
298
|
-
args.length === 2 ? args : [undefined as M, args[0]];
|
|
299
|
-
|
|
300
|
-
const setStateAtPath = ms.setStateAtPath as (
|
|
301
|
-
metadata: M,
|
|
302
|
-
path: P,
|
|
303
|
-
value: GetAtPath<S, P>
|
|
304
|
-
) => void;
|
|
273
|
+
const [metadata, value] = args.length === 2 ? args : [{}, args[0]];
|
|
305
274
|
|
|
306
|
-
setStateAtPath(metadata, (path ?? "") as P, value as never);
|
|
275
|
+
ms.setStateAtPath(metadata, (path ?? "") as P, value as never);
|
|
307
276
|
},
|
|
308
277
|
[ms, path]
|
|
309
278
|
);
|
|
310
279
|
|
|
311
|
-
return setValue
|
|
312
|
-
? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
|
|
313
|
-
: (value: SetStateAction<GetAtPath<S, P>>) => void;
|
|
280
|
+
return setValue;
|
|
314
281
|
}
|
|
315
282
|
|
|
316
283
|
function useValueState(): [
|
|
317
284
|
S,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
285
|
+
(
|
|
286
|
+
...args:
|
|
287
|
+
| [metadata: M, value: SetStateAction<S>]
|
|
288
|
+
| [value: SetStateAction<S>]
|
|
289
|
+
) => void,
|
|
321
290
|
];
|
|
322
291
|
function useValueState<A>(
|
|
323
292
|
selector: (value: S) => A,
|
|
324
293
|
options?: ObservableOptions
|
|
325
294
|
): [
|
|
326
295
|
A,
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
296
|
+
(
|
|
297
|
+
...args:
|
|
298
|
+
| [metadata: M, value: SetStateAction<S>]
|
|
299
|
+
| [value: SetStateAction<S>]
|
|
300
|
+
) => void,
|
|
330
301
|
];
|
|
331
302
|
function useValueState<P extends PathKey[] | string>(
|
|
332
303
|
path?: P
|
|
333
304
|
): [
|
|
334
305
|
GetAtPath<S, P>,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
306
|
+
(
|
|
307
|
+
...args:
|
|
308
|
+
| [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
|
|
309
|
+
| [value: SetStateAction<GetAtPath<S, P>>]
|
|
310
|
+
) => void,
|
|
338
311
|
];
|
|
339
312
|
function useValueState<P extends PathKey[] | string>(
|
|
340
313
|
...args: [P?] | [selector: (value: S) => any, options?: ObservableOptions]
|
|
@@ -350,11 +323,9 @@ export function createNoyaContext<
|
|
|
350
323
|
}
|
|
351
324
|
|
|
352
325
|
function useStateManager() {
|
|
353
|
-
const { ms } = useAnyNoyaStateContext()
|
|
354
|
-
ms: MultiplayerStateManager<S, M>;
|
|
355
|
-
};
|
|
326
|
+
const { multiplayerStateManager: ms } = useAnyNoyaStateContext();
|
|
356
327
|
|
|
357
|
-
return ms
|
|
328
|
+
return ms as MultiplayerStateManager<S, M>;
|
|
358
329
|
}
|
|
359
330
|
|
|
360
331
|
function useEphemeralUserDataManager() {
|
|
@@ -369,6 +340,34 @@ export function createNoyaContext<
|
|
|
369
340
|
return ephemeralUserData;
|
|
370
341
|
}
|
|
371
342
|
|
|
343
|
+
function useNoyaManager() {
|
|
344
|
+
return useAnyNoyaStateContext() as NoyaManager<S, M, E, MenuT>;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function useLeftMenuItems() {
|
|
348
|
+
const { menuManager } = useNoyaManager();
|
|
349
|
+
return useObservable(menuManager.leftMenuItems$);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function useRightMenuItems() {
|
|
353
|
+
const { menuManager } = useNoyaManager();
|
|
354
|
+
return useObservable(menuManager.rightMenuItems$);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function useHandleMenuItem(callback: (type: MenuT) => void) {
|
|
358
|
+
const { menuManager } = useNoyaManager();
|
|
359
|
+
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
return menuManager.addListener(callback);
|
|
362
|
+
}, [menuManager, callback]);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function useOnSelectMenuItemCallback(): (type: MenuT) => void {
|
|
366
|
+
const { menuManager } = useNoyaManager();
|
|
367
|
+
|
|
368
|
+
return useCallback((type: MenuT) => menuManager.emit(type), [menuManager]);
|
|
369
|
+
}
|
|
370
|
+
|
|
372
371
|
return {
|
|
373
372
|
Context: NoyaStateContext,
|
|
374
373
|
Provider: NoyaStateProvider,
|
|
@@ -377,8 +376,10 @@ export function createNoyaContext<
|
|
|
377
376
|
useValueState,
|
|
378
377
|
useStateManager,
|
|
379
378
|
useEphemeralUserDataManager,
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
379
|
+
useLeftMenuItems,
|
|
380
|
+
useRightMenuItems,
|
|
381
|
+
useHandleMenuItem,
|
|
382
|
+
useOnSelectMenuItemCallback,
|
|
383
|
+
useNoyaManager,
|
|
383
384
|
};
|
|
384
385
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientToServerMessage,
|
|
3
|
+
MultiplayerStateManagerError,
|
|
4
|
+
ReconnectingWebSocket,
|
|
5
|
+
ReconnectingWebSocketState,
|
|
6
|
+
ServerToClientMessage,
|
|
7
|
+
UserActivityDetector,
|
|
8
|
+
} from "@noya-app/state-manager";
|
|
9
|
+
|
|
10
|
+
export type ConnectionEvent<State> =
|
|
11
|
+
| {
|
|
12
|
+
type: "stateChange";
|
|
13
|
+
state: ReconnectingWebSocketState;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: "send";
|
|
17
|
+
message: ClientToServerMessage<State>;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
type: "receive";
|
|
21
|
+
message: ServerToClientMessage<State>;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
type: "error";
|
|
25
|
+
error: MultiplayerStateManagerError;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ConnectionOptions<State> = {
|
|
29
|
+
debug?: boolean;
|
|
30
|
+
onConnectionEvent?: (event: ConnectionEvent<State>) => void;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class WebSocketConnection<State> {
|
|
34
|
+
private ws: ReconnectingWebSocket;
|
|
35
|
+
private pingCount = 0;
|
|
36
|
+
private pendingPingId?: string;
|
|
37
|
+
private intervalId?: NodeJS.Timer;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private url: URL,
|
|
41
|
+
private options: ConnectionOptions<State> = {}
|
|
42
|
+
) {
|
|
43
|
+
this.ws = new ReconnectingWebSocket({
|
|
44
|
+
debug: options.debug,
|
|
45
|
+
onopen: this.handleOpen,
|
|
46
|
+
onmessage: this.handleMessage,
|
|
47
|
+
onclose: this.handleClose,
|
|
48
|
+
activityDetector: new UserActivityDetector(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async connect() {
|
|
53
|
+
this.ws.connect(this.url.toString());
|
|
54
|
+
|
|
55
|
+
// Start ping interval
|
|
56
|
+
this.intervalId = setInterval(() => {
|
|
57
|
+
if (this.ws.state !== "OPEN") return;
|
|
58
|
+
|
|
59
|
+
// A response to a previous ping hasn't been received, so we'll reset the connection
|
|
60
|
+
if (this.pendingPingId) {
|
|
61
|
+
this.ws.close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pingId = (this.pingCount++).toString();
|
|
66
|
+
|
|
67
|
+
const message: ClientToServerMessage<State> = {
|
|
68
|
+
type: "ping",
|
|
69
|
+
id: pingId,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.pendingPingId = message.id;
|
|
73
|
+
|
|
74
|
+
this.options.onConnectionEvent?.({
|
|
75
|
+
type: "send",
|
|
76
|
+
message,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.ws.send(JSON.stringify(message));
|
|
80
|
+
}, 10000);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private handleOpen = () => {
|
|
84
|
+
this.pendingPingId = undefined;
|
|
85
|
+
|
|
86
|
+
if (this.options.debug) {
|
|
87
|
+
console.info("ws connected");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.options.onConnectionEvent?.({
|
|
91
|
+
type: "stateChange",
|
|
92
|
+
state: "OPEN",
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
private handleMessage = (event: MessageEvent) => {
|
|
97
|
+
if (this.options.debug) {
|
|
98
|
+
console.info("ws receiving message ", event.data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parsed = JSON.parse(event.data) as ServerToClientMessage<State>;
|
|
102
|
+
|
|
103
|
+
this.options.onConnectionEvent?.({
|
|
104
|
+
type: "receive",
|
|
105
|
+
message: parsed,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (parsed.type === "pong" && parsed.id === this.pendingPingId) {
|
|
109
|
+
this.pendingPingId = undefined;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
private handleClose = () => {
|
|
114
|
+
this.pendingPingId = undefined;
|
|
115
|
+
|
|
116
|
+
if (this.options.debug) {
|
|
117
|
+
console.info("ws disconnected");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.options.onConnectionEvent?.({
|
|
121
|
+
type: "stateChange",
|
|
122
|
+
state: "CLOSED",
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
send(message: ClientToServerMessage<State>) {
|
|
127
|
+
if (this.options.debug) {
|
|
128
|
+
console.info("ws sending message", message);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.options.onConnectionEvent?.({
|
|
132
|
+
type: "send",
|
|
133
|
+
message,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.ws.send(JSON.stringify(message));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
close() {
|
|
140
|
+
if (this.intervalId) {
|
|
141
|
+
clearInterval(this.intervalId);
|
|
142
|
+
}
|
|
143
|
+
this.ws.shutdown();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
addListener(listener: (state: ReconnectingWebSocketState) => void) {
|
|
147
|
+
return this.ws.addListener(listener);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
removeListener(listener: (state: ReconnectingWebSocketState) => void) {
|
|
151
|
+
return this.ws.removeListener(listener);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MultiplayerStateManagerError } from "@noya-app/state-manager";
|
|
2
|
-
import React from "react";
|
|
2
|
+
import * as React from "react";
|
|
3
3
|
|
|
4
|
-
export function ErrorOverlay({
|
|
4
|
+
export const ErrorOverlay = React.memo(function ErrorOverlay({
|
|
5
5
|
error,
|
|
6
6
|
}: {
|
|
7
7
|
error: MultiplayerStateManagerError;
|
|
@@ -86,4 +86,4 @@ export function ErrorOverlay({
|
|
|
86
86
|
</button>
|
|
87
87
|
</div>
|
|
88
88
|
);
|
|
89
|
-
}
|
|
89
|
+
});
|