@noya-app/noya-multiplayer-react 0.1.37 → 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.
- package/.turbo/turbo-build.log +11 -12
- package/CHANGELOG.md +17 -0
- package/dist/index.bundle.js +7 -7
- package/dist/index.d.mts +53 -17
- package/dist/index.d.ts +53 -17
- package/dist/index.js +122 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/NoyaStateContext.tsx +218 -0
- package/src/__tests__/NoyaStateContext.test.ts +43 -0
- package/src/hooks.ts +12 -3
- package/src/index.ts +1 -0
- package/src/noyaApp.ts +4 -4
- package/src/useObservable.ts +6 -6
|
@@ -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
|
-
):
|
|
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
|
-
|
|
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
package/src/noyaApp.ts
CHANGED
|
@@ -165,7 +165,7 @@ export type UseNoyaStateOptions<
|
|
|
165
165
|
M = void,
|
|
166
166
|
E = void,
|
|
167
167
|
> = UseMultiplayerStateOptions<S, M, E> & {
|
|
168
|
-
|
|
168
|
+
offlineStorageKey?: string;
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
export type UseNoyaStateResult<S, M = void, E = void> = [
|
|
@@ -231,10 +231,10 @@ export function useNoyaState<
|
|
|
231
231
|
const sync = useMemo((): SyncAdapter<S, M, E> => {
|
|
232
232
|
return multiplayerUrl
|
|
233
233
|
? webSocketSync(multiplayerUrl)
|
|
234
|
-
: options?.
|
|
235
|
-
? localStorageSync({ key: options.
|
|
234
|
+
: options?.offlineStorageKey
|
|
235
|
+
? localStorageSync({ key: options.offlineStorageKey })
|
|
236
236
|
: stubSync();
|
|
237
|
-
}, [multiplayerUrl, options?.
|
|
237
|
+
}, [multiplayerUrl, options?.offlineStorageKey]);
|
|
238
238
|
|
|
239
239
|
const result = useMultiplayerState(noyaAppInitialState as S, {
|
|
240
240
|
...options,
|
package/src/useObservable.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import {
|
|
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
|
-
):
|
|
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:
|
|
16
|
-
observable.
|
|
17
|
-
: observable.
|
|
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
|
|