@noya-app/noya-multiplayer-react 0.1.35 → 0.1.37

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.
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
+ AssetManager,
4
5
  ConnectionEvent,
5
6
  EphemeralUserData,
6
7
  ExtendedPathKey,
@@ -26,6 +27,7 @@ import {
26
27
  } from "react-inspector";
27
28
  import { shouldTrackEvents$, shouldTrackTasks$ } from "../globals";
28
29
  import { useEphemeralUserData, useManagedHistory } from "../hooks";
30
+ import { useObservable } from "../useObservable";
29
31
  import { useLocalStorageState } from "./useLocalStorageState";
30
32
 
31
33
  const lightTheme = {
@@ -258,6 +260,7 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
258
260
  unstyled,
259
261
  colorScheme = "light",
260
262
  multiplayerStateManager,
263
+ assetManager,
261
264
  ephemeralUserData,
262
265
  anchor = "right",
263
266
  forceInit,
@@ -270,6 +273,7 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
270
273
  userId?: string;
271
274
  unstyled?: boolean;
272
275
  multiplayerStateManager: MultiplayerStateManager<S, M>;
276
+ assetManager: AssetManager;
273
277
  ephemeralUserData: EphemeralUserData<E>;
274
278
  forceInit: () => void;
275
279
  } & StateInspectorOptions &
@@ -311,6 +315,10 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
311
315
  "noya-multiplayer-react-show-ephemeral",
312
316
  false
313
317
  );
318
+ const [showAssets, setShowAssets] = useLocalStorageState(
319
+ "noya-multiplayer-react-show-assets",
320
+ false
321
+ );
314
322
 
315
323
  useEffect(() => {
316
324
  shouldTrackEvents$.set(showEvents);
@@ -329,6 +337,7 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
329
337
 
330
338
  const ephemeral = useEphemeralUserData(ephemeralUserData);
331
339
  const historySnapshot = useManagedHistory(multiplayerStateManager.sm);
340
+ const assets = useObservable(assetManager.assets$);
332
341
 
333
342
  useEffect(() => {
334
343
  if (historyContainerRef.current) {
@@ -680,6 +689,52 @@ export const StateInspector = memo(function StateInspector<S, M, E>({
680
689
  ))}
681
690
  </div>
682
691
  </DisclosureSection>
692
+ <DisclosureSection
693
+ open={showAssets}
694
+ setOpen={setShowAssets}
695
+ title="Assets"
696
+ colorScheme={colorScheme}
697
+ right={
698
+ <span style={{ display: "flex", gap: "4px" }}>
699
+ <SmallButton
700
+ theme={theme}
701
+ onClick={() => {
702
+ // get file input
703
+ const input = document.createElement("input");
704
+ input.type = "file";
705
+ input.onchange = () => {
706
+ const file = input.files?.[0];
707
+ if (file) {
708
+ const reader = new FileReader();
709
+ reader.onload = () => {
710
+ const buffer = reader.result as ArrayBuffer;
711
+ assetManager.create(new Uint8Array(buffer));
712
+ };
713
+ reader.readAsArrayBuffer(file);
714
+ }
715
+ };
716
+ input.click();
717
+ }}
718
+ >
719
+ Upload
720
+ </SmallButton>
721
+ </span>
722
+ }
723
+ >
724
+ <div style={styles.sectionInner}>
725
+ {assets.map((asset) => (
726
+ <InspectorRow key={asset.id} colorScheme={colorScheme}>
727
+ <ObjectInspector name={asset.id} data={asset} theme={theme} />
728
+ <SmallButton
729
+ theme={theme}
730
+ onClick={() => assetManager.delete(asset.id)}
731
+ >
732
+ Delete
733
+ </SmallButton>
734
+ </InspectorRow>
735
+ ))}
736
+ </div>
737
+ </DisclosureSection>
683
738
  <DisclosureSection
684
739
  title="Tasks"
685
740
  colorScheme={colorScheme}
@@ -939,7 +994,7 @@ function SmallButton({
939
994
  display: "inline-flex",
940
995
  alignItems: "center",
941
996
  justifyContent: "center",
942
- padding: "2px 0",
997
+ padding: "0",
943
998
  cursor: "pointer",
944
999
  ...style,
945
1000
  }}
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
+ AssetManager,
4
5
  ConnectionEvent,
5
6
  EphemeralUserData,
6
7
  MultiplayerStateManager,
@@ -13,10 +14,13 @@ import { createRoot } from "react-dom/client";
13
14
  import { Root } from "react-dom/client";
14
15
  import { StateInspector } from "./StateInspector";
15
16
 
16
- function createPortalElement() {
17
- const el = document.createElement("div");
18
- el.id = "noya-multiplayer-react-inspector-portal";
19
- return el;
17
+ function createShadowRootElement() {
18
+ const host = document.createElement("div");
19
+ host.id = "noya-multiplayer-react-inspector-host";
20
+ const shadowRoot = host.attachShadow({ mode: "open" });
21
+ const container = document.createElement("div");
22
+ shadowRoot.appendChild(container);
23
+ return { host, container };
20
24
  }
21
25
 
22
26
  export function useStateInspector<S, M, E>({
@@ -29,6 +33,7 @@ export function useStateInspector<S, M, E>({
29
33
  colorScheme,
30
34
  anchor,
31
35
  multiplayerStateManager,
36
+ assetManager,
32
37
  ephemeralUserData,
33
38
  forceInit,
34
39
  }: {
@@ -39,6 +44,7 @@ export function useStateInspector<S, M, E>({
39
44
  userId?: string;
40
45
  disabled: boolean;
41
46
  multiplayerStateManager: MultiplayerStateManager<S, M>;
47
+ assetManager: AssetManager;
42
48
  ephemeralUserData: EphemeralUserData<E>;
43
49
  forceInit: () => void;
44
50
  } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
@@ -47,16 +53,15 @@ export function useStateInspector<S, M, E>({
47
53
  useLayoutEffect(() => {
48
54
  if (disabled) return;
49
55
 
50
- const portalElement = createPortalElement();
56
+ const { host, container } = createShadowRootElement();
57
+ const root = createRoot(container);
51
58
 
52
- const root = createRoot(portalElement);
53
-
54
- document.body.appendChild(portalElement);
59
+ document.body.appendChild(host);
55
60
 
56
61
  setRoot(root);
57
62
 
58
63
  return () => {
59
- document.body.removeChild(portalElement);
64
+ document.body.removeChild(host);
60
65
  };
61
66
  }, [disabled]);
62
67
 
@@ -76,6 +81,7 @@ export function useStateInspector<S, M, E>({
76
81
  multiplayerStateManager={
77
82
  multiplayerStateManager as MultiplayerStateManager<any, any>
78
83
  }
84
+ assetManager={assetManager}
79
85
  ephemeralUserData={ephemeralUserData as EphemeralUserData<any>}
80
86
  />
81
87
  );
@@ -91,5 +97,6 @@ export function useStateInspector<S, M, E>({
91
97
  tasks,
92
98
  forceInit,
93
99
  ephemeralUserData,
100
+ assetManager,
94
101
  ]);
95
102
  }
package/src/noyaApp.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  stubSync,
9
9
  SyncAdapter,
10
10
  TSchema,
11
+ TypeGuard,
11
12
  webSocketSync,
12
13
  } from "@noya-app/state-manager";
13
14
  import { useEffect, useMemo, useState } from "react";
@@ -159,7 +160,7 @@ export function isEmbeddedApp() {
159
160
  return window.self !== window.top;
160
161
  }
161
162
 
162
- export type UseNoyaAppStateOptions<
163
+ export type UseNoyaStateOptions<
163
164
  S,
164
165
  M = void,
165
166
  E = void,
@@ -167,7 +168,7 @@ export type UseNoyaAppStateOptions<
167
168
  localStorageKey?: string;
168
169
  };
169
170
 
170
- export type UseNoyaAppStateResult<S, M = void, E = void> = [
171
+ export type UseNoyaStateResult<S, M = void, E = void> = [
171
172
  ReturnType<typeof useMultiplayerState<S, M, E>>[0],
172
173
  ReturnType<typeof useMultiplayerState<S, M, E>>[1],
173
174
  ReturnType<typeof useMultiplayerState<S, M, E>>[2] & {
@@ -176,17 +177,42 @@ export type UseNoyaAppStateResult<S, M = void, E = void> = [
176
177
  },
177
178
  ];
178
179
 
179
- export function useNoyaAppState<
180
+ /**
181
+ * Sync data locally or to a multiplayer server.
182
+ *
183
+ * There are 3 ways to use this hook:
184
+ *
185
+ * 1. Pass an initial state: `useNoyaState(initialState)`
186
+ * 2. Pass an initial state and a schema: `useNoyaState(initialState, { schema })`
187
+ * 3. Pass only a schema, and a conforming initial state is created automatically: `useNoyaState({ schema })`
188
+ */
189
+ export function useNoyaState<
180
190
  S,
181
191
  M = void,
182
192
  E = void,
183
- O extends UseNoyaAppStateOptions<S, M, E> = UseNoyaAppStateOptions<S, M, E>,
193
+ O extends UseNoyaStateOptions<S, M, E> = UseNoyaStateOptions<S, M, E>,
184
194
  >(
185
- initialState: O["schema"] extends TSchema
186
- ? Static<O["schema"]> | (() => Static<O["schema"]>)
187
- : S | (() => S),
188
- options?: O
189
- ) {
195
+ ...args:
196
+ | [
197
+ initialState: O["schema"] extends TSchema
198
+ ? Static<O["schema"]> | (() => Static<O["schema"]>)
199
+ : S | (() => S),
200
+ options?: O,
201
+ ]
202
+ | [options: O & { schema: TSchema }]
203
+ ): UseNoyaStateResult<
204
+ O["schema"] extends TSchema ? Static<O["schema"]> : S,
205
+ M,
206
+ E
207
+ > {
208
+ const [initialState, options] =
209
+ typeof args[0] === "object" &&
210
+ args[0] !== null &&
211
+ "schema" in args[0] &&
212
+ TypeGuard.IsSchema(args[0].schema)
213
+ ? [null, args[0]]
214
+ : [args[0], args[1]];
215
+
190
216
  // Keep these stable after initial render
191
217
  const [
192
218
  {
@@ -224,9 +250,19 @@ export function useNoyaAppState<
224
250
  return { ...extras, theme, viewType } as const;
225
251
  }, [extras, theme, viewType]);
226
252
 
227
- return [result[0], result[1], mergedExtras] as UseNoyaAppStateResult<
253
+ return [result[0], result[1], mergedExtras] as UseNoyaStateResult<
228
254
  O["schema"] extends TSchema ? Static<O["schema"]> : S,
229
255
  M,
230
256
  E
231
257
  >;
232
258
  }
259
+
260
+ // function typeTest() {
261
+ // const [a, setA] = useNoyaState(7);
262
+ // const [b, setB] = useNoyaState(7, {
263
+ // schema: Type.Number(),
264
+ // });
265
+ // const [c, setC] = useNoyaState({
266
+ // schema: Type.Number(),
267
+ // });
268
+ // }