@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.
@@ -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
- SecretManager,
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<S, M, E extends object>
30
- extends Omit<UseNoyaStateOptions<S, M, E>, "schema" | "mergeHistoryEntries"> {
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 { ms, secretManager, assetManager } = useAnyNoyaStateContext();
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 = void,
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={value}>
236
- <ConnectedUsersContext.Provider value={connectedUsersManager}>
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(): M extends {}
281
- ? (metadata: M, value: SetStateAction<S>) => void
282
- : (value: SetStateAction<S>) => void;
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
- ): M extends {}
286
- ? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
287
- : (value: SetStateAction<GetAtPath<S, P>>) => void;
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: M extends {}
294
- ? [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
295
- : [value: SetStateAction<GetAtPath<S, P>>]
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 as M extends {}
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
- M extends {}
319
- ? (metadata: M, value: SetStateAction<S>) => void
320
- : (value: SetStateAction<S>) => void,
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
- M extends {}
328
- ? (metadata: M, value: SetStateAction<S>) => void
329
- : (value: SetStateAction<S>) => void,
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
- M extends {}
336
- ? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
337
- : (value: SetStateAction<GetAtPath<S, P>>) => void,
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() as {
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
- // useAssets,
381
- // useAssetManager,
382
- // useAsset,
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
+ });