@noya-app/noya-multiplayer-react 0.1.38 → 0.1.39

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.
@@ -0,0 +1,218 @@
1
+ import { GetAtPath, ObservableOptions, PathKey } from "@noya-app/observable";
2
+ import {
3
+ AssetManager,
4
+ MultiplayerStateManager,
5
+ Static,
6
+ TSchema,
7
+ } from "@noya-app/state-manager";
8
+ import React, {
9
+ createContext,
10
+ SetStateAction,
11
+ useCallback,
12
+ useContext,
13
+ useMemo,
14
+ } from "react";
15
+ import { useNoyaState, UseNoyaStateOptions } from "./noyaApp";
16
+ import { useObservable } from "./useObservable";
17
+
18
+ interface NoyaStateProviderProps<Schema extends TSchema, M, E>
19
+ extends Omit<UseNoyaStateOptions<Static<Schema>, M, E>, "schema"> {
20
+ children: React.ReactNode;
21
+ initialState?: Static<Schema>;
22
+ }
23
+
24
+ const AnyNoyaStateContext = createContext<
25
+ | { ms: MultiplayerStateManager<any, any>; assetManager: AssetManager }
26
+ | undefined
27
+ >(undefined);
28
+
29
+ function useAnyNoyaStateContext() {
30
+ const value = useContext(AnyNoyaStateContext);
31
+
32
+ if (!value) {
33
+ throw new Error(
34
+ "useNoyaStateContext must be used within a NoyaStateProvider"
35
+ );
36
+ }
37
+
38
+ return value;
39
+ }
40
+
41
+ export function useAssets() {
42
+ const { assetManager } = useAnyNoyaStateContext();
43
+ return useObservable(assetManager.assets$);
44
+ }
45
+
46
+ export function useAsset(id: string | undefined) {
47
+ const { assetManager } = useAnyNoyaStateContext();
48
+ const observable = useMemo(
49
+ () => assetManager.assets$.map((assets) => assets.find((a) => a.id === id)),
50
+ [assetManager, id]
51
+ );
52
+ return useObservable(observable);
53
+ }
54
+
55
+ export function useAssetManager() {
56
+ const { assetManager } = useAnyNoyaStateContext();
57
+ return useMemo(
58
+ () => ({
59
+ create: assetManager.create,
60
+ delete: assetManager.delete,
61
+ }),
62
+ [assetManager]
63
+ );
64
+ }
65
+
66
+ export function createNoyaContext<Schema extends TSchema, M = void, E = void>({
67
+ schema,
68
+ }: {
69
+ schema: Schema;
70
+ }) {
71
+ const NoyaStateContext = AnyNoyaStateContext as React.Context<
72
+ | {
73
+ ms: MultiplayerStateManager<Static<Schema>, M>;
74
+ assetManager: AssetManager;
75
+ }
76
+ | undefined
77
+ >;
78
+
79
+ function NoyaStateProvider({
80
+ children,
81
+ initialState,
82
+ ...options
83
+ }: NoyaStateProviderProps<Schema, M, E>) {
84
+ const [, , { multiplayerStateManager, assetManager }] = useNoyaState(
85
+ initialState,
86
+ {
87
+ ...(options as any),
88
+ schema,
89
+ }
90
+ );
91
+
92
+ const value = useMemo(
93
+ () => ({
94
+ ms: multiplayerStateManager as MultiplayerStateManager<any, any>,
95
+ assetManager,
96
+ }),
97
+ [multiplayerStateManager, assetManager]
98
+ );
99
+
100
+ return (
101
+ <AnyNoyaStateContext.Provider value={value}>
102
+ {children}
103
+ </AnyNoyaStateContext.Provider>
104
+ );
105
+ }
106
+
107
+ type S = Static<Schema>;
108
+
109
+ function useValue(): S;
110
+ function useValue<A>(
111
+ selector: (value: S) => A,
112
+ options?: Pick<ObservableOptions, "isEqual">
113
+ ): A;
114
+ function useValue<P extends PathKey[] | string>(path: P): GetAtPath<S, P>;
115
+ function useValue(
116
+ path?: PathKey[] | string | ((value: S) => any),
117
+ options?: Pick<ObservableOptions, "isEqual">
118
+ ) {
119
+ const { ms } = useAnyNoyaStateContext();
120
+
121
+ let actualPath: PathKey[] | string =
122
+ typeof path === "function" || !path ? "" : path;
123
+
124
+ const mappedObservable = useMemo(() => {
125
+ if (typeof path === "function") {
126
+ return ms.optimisticState$.map(path, { isEqual: options?.isEqual });
127
+ }
128
+ return ms.optimisticState$;
129
+ }, [ms, path, options?.isEqual]);
130
+
131
+ const value = useObservable(mappedObservable, actualPath);
132
+
133
+ return value;
134
+ }
135
+
136
+ function useSetValue(): M extends {}
137
+ ? (metadata: M, value: SetStateAction<S>) => void
138
+ : (value: SetStateAction<S>) => void;
139
+ function useSetValue<P extends PathKey[] | string>(
140
+ path: P
141
+ ): M extends {}
142
+ ? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
143
+ : (value: SetStateAction<GetAtPath<S, P>>) => void;
144
+ function useSetValue<P extends PathKey[] | string>(path?: P) {
145
+ const { ms } = useAnyNoyaStateContext();
146
+
147
+ const setValue = useCallback(
148
+ (
149
+ ...args: M extends {}
150
+ ? [metadata: M, value: SetStateAction<GetAtPath<S, P>>]
151
+ : [value: SetStateAction<GetAtPath<S, P>>]
152
+ ): void => {
153
+ const [metadata, value] =
154
+ args.length === 2 ? args : [undefined as M, args[0]];
155
+
156
+ const setStateAtPath = ms.setStateAtPath as (
157
+ metadata: M,
158
+ path: P,
159
+ value: GetAtPath<S, P>
160
+ ) => void;
161
+
162
+ setStateAtPath(metadata, (path ?? "") as P, value as never);
163
+ },
164
+ [ms, path]
165
+ );
166
+
167
+ return setValue as M extends {}
168
+ ? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
169
+ : (value: SetStateAction<GetAtPath<S, P>>) => void;
170
+ }
171
+
172
+ function useValueState(): [
173
+ S,
174
+ M extends {}
175
+ ? (metadata: M, value: SetStateAction<S>) => void
176
+ : (value: SetStateAction<S>) => void,
177
+ ];
178
+ function useValueState<A>(
179
+ selector: (value: S) => A,
180
+ options?: ObservableOptions
181
+ ): [
182
+ A,
183
+ M extends {}
184
+ ? (metadata: M, value: SetStateAction<S>) => void
185
+ : (value: SetStateAction<S>) => void,
186
+ ];
187
+ function useValueState<P extends PathKey[] | string>(
188
+ path?: P
189
+ ): [
190
+ GetAtPath<S, P>,
191
+ M extends {}
192
+ ? (metadata: M, value: SetStateAction<GetAtPath<S, P>>) => void
193
+ : (value: SetStateAction<GetAtPath<S, P>>) => void,
194
+ ];
195
+ function useValueState<P extends PathKey[] | string>(
196
+ ...args: [P?] | [selector: (value: S) => any, options?: ObservableOptions]
197
+ ) {
198
+ const [selector, options, path] =
199
+ args.length >= 1 && typeof args[0] === "function"
200
+ ? [args[0], args[1], undefined]
201
+ : [undefined, undefined, args[0] as P | undefined];
202
+
203
+ const value = useValue((selector ?? path ?? "") as any, options);
204
+ const setValue = useSetValue(path ?? "");
205
+ return [value, setValue];
206
+ }
207
+
208
+ return {
209
+ Context: NoyaStateContext,
210
+ Provider: NoyaStateProvider,
211
+ useValue,
212
+ useSetValue,
213
+ useValueState,
214
+ // useAssets,
215
+ // useAssetManager,
216
+ // useAsset,
217
+ };
218
+ }
@@ -0,0 +1,43 @@
1
+ import { Type } from "@noya-app/state-manager";
2
+ import { it } from "bun:test";
3
+ import { useCallback } from "react";
4
+ import { createNoyaContext } from "../NoyaStateContext";
5
+
6
+ it("typechecks", () => {
7
+ const testSchema = Type.Object({
8
+ count: Type.Number(),
9
+ });
10
+
11
+ const { useValue, useSetValue, useValueState } = createNoyaContext({
12
+ schema: testSchema,
13
+ });
14
+
15
+ // For type checking
16
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
17
+ function TestComponent() {
18
+ const state = useValue();
19
+ const count = useValue("count");
20
+ const setValue = useSetValue();
21
+ const setCount = useSetValue("count");
22
+ setValue({ count: 1 });
23
+ setCount(1);
24
+ setCount((prev) => prev + 1);
25
+ const [count2, setCount2] = useValueState("count");
26
+ setCount2(1);
27
+ setCount2((prev) => prev + 1);
28
+ const count3 = useValue(useCallback((state) => state.count, []));
29
+ const [count4, setValue2] = useValueState(
30
+ useCallback((state) => state.count, [])
31
+ );
32
+ setValue2({ count: 1 });
33
+ setValue2((prev) => ({ count: prev.count + 1 }));
34
+
35
+ expectType<{ count: number }>(state);
36
+ expectType<number>(count);
37
+ expectType<number>(count2);
38
+ expectType<number>(count3);
39
+ expectType<number>(count4);
40
+ }
41
+ });
42
+
43
+ function expectType<T>(value: T): void {}
package/src/hooks.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
 
3
+ import { GetAtPath } from "@noya-app/observable";
3
4
  import {
4
5
  ActionHandler,
5
6
  AssetManager,
6
7
  ConnectionEvent,
7
8
  EphemeralUserData,
8
- Get,
9
9
  MultiplayerStateManager,
10
10
  MultiplayerStateManagerError,
11
11
  MultiplayerStateManagerOptions,
@@ -38,7 +38,7 @@ export function useSyncStateManager<S, M>(stateManager: StateManager<S, M>): S;
38
38
  export function useSyncStateManager<S, M, P extends string>(
39
39
  stateManager: StateManager<S, M>,
40
40
  path: P
41
- ): Get<S, P, unknown>;
41
+ ): GetAtPath<S, P>;
42
42
  export function useSyncStateManager<S, M>(
43
43
  stateManager: StateManager<S, M>,
44
44
  path?: string
@@ -241,6 +241,7 @@ export function useMultiplayerState<S, M = void, E = void>(
241
241
  ephemeralUserData,
242
242
  multiplayerStateManager,
243
243
  assets,
244
+ assetManager,
244
245
  createAsset: assetManager.create,
245
246
  deleteAsset: assetManager.delete,
246
247
  // evaluate: async (code: string) => {
@@ -338,7 +339,15 @@ export function useMultiplayerState<S, M = void, E = void>(
338
339
 
339
340
  useEffect(() => {
340
341
  return multiplayerStateManager.errorEmitter.addListener((error) => {
341
- setUnrecoverableError(error);
342
+ switch (error.reason) {
343
+ case "invalidDataType":
344
+ case "schemaMismatch":
345
+ case "outdatedSchema":
346
+ case "dataMigrationFailure":
347
+ case "schemaMigration":
348
+ setUnrecoverableError(error);
349
+ break;
350
+ }
342
351
  });
343
352
  }, [multiplayerStateManager]);
344
353
 
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from "./components/UserPointersOverlay";
4
4
  export * from "./hooks";
5
5
  export * from "./inspector/StateInspector";
6
6
  export * from "./noyaApp";
7
+ export * from "./NoyaStateContext";
7
8
  export * from "./useObservable";
@@ -1,20 +1,20 @@
1
- import { GetWithPath, Observable, PathKey } from "@noya-app/observable";
1
+ import { GetAtPath, Observable, PathKey } from "@noya-app/observable";
2
2
  import { useMemo, useSyncExternalStore } from "react";
3
3
 
4
4
  export function useObservable<T>(observable: Observable<T>): T;
5
- export function useObservable<T, P extends PathKey[]>(
5
+ export function useObservable<T, P extends PathKey[] | string>(
6
6
  observable: Observable<T>,
7
7
  path: P
8
- ): GetWithPath<T, P>;
8
+ ): GetAtPath<T, P>;
9
9
  export function useObservable<T, P extends PathKey[]>(
10
10
  observable: Observable<T>,
11
11
  path?: P
12
12
  ) {
13
13
  const { get, listen } = useMemo(() => {
14
14
  const listen = path
15
- ? (callback: (value: GetWithPath<T, P>) => void) =>
16
- observable.listen(path, callback)
17
- : observable.listen;
15
+ ? (callback: (value: GetAtPath<T, P>) => void) =>
16
+ observable.subscribe(path, callback)
17
+ : observable.subscribe;
18
18
 
19
19
  const get: () => void = path ? () => observable.get(path) : observable.get;
20
20