@noya-app/noya-multiplayer-react 0.1.70 → 0.1.72

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.
@@ -8,12 +8,14 @@ export function StateInspectorRow({
8
8
  selected,
9
9
  style,
10
10
  variant,
11
+ bordered = true,
11
12
  }: {
12
13
  children: React.ReactNode;
13
14
  colorScheme: "light" | "dark";
14
15
  selected?: boolean;
15
16
  style?: CSSProperties;
16
17
  variant?: "up" | "down";
18
+ bordered?: boolean;
17
19
  }) {
18
20
  const solidBorderColor =
19
21
  colorScheme === "light" ? "rgb(223 223 223)" : "rgb(29 29 29)";
@@ -21,7 +23,7 @@ export function StateInspectorRow({
21
23
  return (
22
24
  <div
23
25
  style={{
24
- borderBottom: `1px solid ${solidBorderColor}`,
26
+ borderBottom: bordered ? `1px solid ${solidBorderColor}` : undefined,
25
27
  fontSize: "12px",
26
28
  fontFamily: "Menlo, monospace",
27
29
  padding: "2px 12px 1px",
@@ -0,0 +1,128 @@
1
+ import { ServerScriptLogEntry } from "@noya-app/state-manager";
2
+ import React from "react";
3
+ import { ObjectInspector } from "react-inspector";
4
+ import { getStateInspectorTheme } from "../inspectorTheme";
5
+ import { StateInspectorButton } from "../StateInspectorButton";
6
+ import {
7
+ StateInspectorDisclosureRowInner,
8
+ StateInspectorDisclosureSection,
9
+ } from "../StateInspectorDisclosureSection";
10
+ import { StateInspectorRow } from "../StateInspectorRow";
11
+
12
+ const levelColors: Record<ServerScriptLogEntry["level"], string> = {
13
+ error: "#f87171",
14
+ warn: "#facc15",
15
+ log: "#9ca3af",
16
+ };
17
+
18
+ function formatTimestamp(timestamp: number) {
19
+ return new Date(timestamp).toLocaleTimeString();
20
+ }
21
+
22
+ export function ServerScriptLogsSection({
23
+ showLogs,
24
+ setShowLogs,
25
+ colorScheme,
26
+ logs,
27
+ containerRef,
28
+ paused,
29
+ onTogglePaused,
30
+ }: {
31
+ showLogs: boolean;
32
+ setShowLogs: (value: boolean) => void;
33
+ colorScheme: "light" | "dark";
34
+ logs: ServerScriptLogEntry[];
35
+ containerRef: React.RefObject<HTMLDivElement | null>;
36
+ paused: boolean;
37
+ onTogglePaused: () => void;
38
+ }) {
39
+ const theme = getStateInspectorTheme(colorScheme);
40
+
41
+ return (
42
+ <StateInspectorDisclosureSection
43
+ open={showLogs}
44
+ setOpen={setShowLogs}
45
+ title="Server Script Logs"
46
+ colorScheme={colorScheme}
47
+ right={
48
+ <StateInspectorButton theme={theme} onClick={onTogglePaused}>
49
+ {paused ? "Resume" : "Pause"}
50
+ </StateInspectorButton>
51
+ }
52
+ >
53
+ <StateInspectorDisclosureRowInner ref={containerRef}>
54
+ {paused && (
55
+ <div
56
+ style={{
57
+ padding: "4px 12px",
58
+ fontSize: "11px",
59
+ opacity: 0.7,
60
+ }}
61
+ >
62
+ Log streaming paused
63
+ </div>
64
+ )}
65
+ {logs.map((log) => (
66
+ <StateInspectorRow key={log.id} colorScheme={colorScheme}>
67
+ <div
68
+ style={{
69
+ display: "flex",
70
+ gap: "6px",
71
+ alignItems: "center",
72
+ fontSize: "10px",
73
+ color: "inherit",
74
+ marginBottom: log.values.length ? "4px" : 0,
75
+ textTransform: "uppercase",
76
+ }}
77
+ >
78
+ <span>{formatTimestamp(log.timestamp)}</span>
79
+ <span style={{ color: levelColors[log.level] }}>{log.level}</span>
80
+ <span
81
+ style={{
82
+ fontFamily: "monospace",
83
+ textTransform: "none",
84
+ fontSize: "10px",
85
+ }}
86
+ >
87
+ {log.scriptId}
88
+ </span>
89
+ </div>
90
+ <div
91
+ style={{
92
+ display: "flex",
93
+ flexDirection: "column",
94
+ gap: "4px",
95
+ }}
96
+ >
97
+ {log.values.length > 0 ? (
98
+ log.values.map((value, index) => (
99
+ <ObjectInspector
100
+ key={index}
101
+ data={value}
102
+ theme={theme}
103
+ expandLevel={3}
104
+ />
105
+ ))
106
+ ) : (
107
+ <span style={{ fontSize: "11px", opacity: 0.7 }}>No data</span>
108
+ )}
109
+ </div>
110
+ </StateInspectorRow>
111
+ ))}
112
+ {!logs?.length && (
113
+ <div
114
+ style={{
115
+ padding: "12px",
116
+ fontSize: "12px",
117
+ display: "flex",
118
+ flexDirection: "column",
119
+ gap: "4px",
120
+ }}
121
+ >
122
+ <span>No logs received</span>
123
+ </div>
124
+ )}
125
+ </StateInspectorDisclosureRowInner>
126
+ </StateInspectorDisclosureSection>
127
+ );
128
+ }
@@ -17,7 +17,9 @@ export async function fetchAssetMap(
17
17
  return Object.fromEntries(res);
18
18
  }
19
19
 
20
- export async function exportAll(noyaManager: NoyaManager<any, any, any, any>) {
20
+ export async function exportAll(
21
+ noyaManager: NoyaManager<any, any, any, any, any>
22
+ ) {
21
23
  const { multiplayerStateManager, assetManager } = noyaManager;
22
24
 
23
25
  const assetMap = await fetchAssetMap(assetManager.assets$.get());
@@ -64,7 +66,7 @@ export function encodeAll({
64
66
 
65
67
  export async function importAll(
66
68
  buffer: Uint8Array | ArrayBuffer,
67
- noyaManager: NoyaManager<any, any, any, any>,
69
+ noyaManager: NoyaManager<any, any, any, any, any>,
68
70
  {
69
71
  shouldAllowSchemaChange,
70
72
  }: {
@@ -35,15 +35,20 @@ export function useStateInspector<
35
35
  M extends object,
36
36
  E extends object,
37
37
  MenuT extends string = string,
38
+ I extends Record<string, any> = Record<string, unknown>,
38
39
  >({
39
40
  noyaManager,
40
41
  disabled = false,
42
+ advanced = false,
41
43
  colorScheme,
42
44
  anchor,
43
45
  }: {
44
- noyaManager: NoyaManager<S, M, E, MenuT>;
45
- disabled: boolean;
46
- } & Pick<ComponentProps<typeof StateInspector>, "colorScheme" | "anchor">) {
46
+ noyaManager: NoyaManager<S, M, E, MenuT, I>;
47
+ disabled?: boolean;
48
+ advanced?: boolean;
49
+ colorScheme?: ComponentProps<typeof StateInspector>["colorScheme"];
50
+ anchor?: ComponentProps<typeof StateInspector>["anchor"];
51
+ }) {
47
52
  const [root, setRoot] = React.useState<Root | null>(null);
48
53
 
49
54
  useIsomorphicLayoutEffect(() => {
@@ -64,7 +69,8 @@ export function useStateInspector<
64
69
  colorScheme={colorScheme}
65
70
  anchor={anchor}
66
71
  noyaManager={noyaManager}
72
+ advanced={advanced}
67
73
  />
68
74
  );
69
- }, [noyaManager, anchor, colorScheme, root]);
75
+ }, [noyaManager, anchor, colorScheme, root, advanced]);
70
76
  }
package/src/noyaApp.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import { findAllDefs } from "@noya-app/noya-schemas";
4
+ import type { LocalStorageSyncOptions } from "@noya-app/state-manager";
4
5
  import {
6
+ AccessType,
5
7
  createOrCastValue,
6
8
  isEmbeddedApp,
7
9
  localStorageSync,
8
10
  NoyaManager,
9
11
  parentFrameSync,
12
+ PolicyAuthContext,
10
13
  Static,
11
14
  stubSync,
12
15
  SyncAdapter,
@@ -14,7 +17,7 @@ import {
14
17
  TypeGuard,
15
18
  webSocketSync,
16
19
  } from "@noya-app/state-manager";
17
- import { useMemo, useState } from "react";
20
+ import { useEffect, useMemo, useState } from "react";
18
21
  import { useBindNoyaManager, UseMultiplayerStateOptions } from "./hooks";
19
22
  import { StateInspectorOptions } from "./inspector/StateInspector";
20
23
 
@@ -28,6 +31,9 @@ export type AppData<State> = {
28
31
  viewType: AppViewType;
29
32
  initialState: State;
30
33
  inspector: boolean | StateInspectorOptions;
34
+ userId?: string;
35
+ isDemo: boolean;
36
+ access?: AccessType;
31
37
  };
32
38
 
33
39
  export function createDefaultAppData<State>(
@@ -38,6 +44,8 @@ export function createDefaultAppData<State>(
38
44
  viewType: "editable",
39
45
  initialState,
40
46
  inspector: false,
47
+ isDemo: false,
48
+ access: undefined,
41
49
  };
42
50
  }
43
51
 
@@ -93,6 +101,10 @@ export function getAppData<State>(
93
101
  }
94
102
 
95
103
  appData.initialState = enforceSchema(appData.initialState, schema) as State;
104
+ appData.isDemo = appData.isDemo ?? false;
105
+ const fallbackAccess: AccessType =
106
+ appData.viewType === "editable" ? "write" : "read";
107
+ appData.access = appData.access ?? fallbackAccess;
96
108
 
97
109
  return appData as AppData<State>;
98
110
  }
@@ -102,20 +114,32 @@ export function getSyncAdapter<
102
114
  M extends object = object,
103
115
  E extends object = object,
104
116
  MenuT extends string = string,
117
+ I extends Record<string, any> = Record<string, unknown>,
105
118
  >(params?: {
106
119
  multiplayerUrl?: string;
107
120
  offlineStorageKey?: string;
108
121
  debug?: boolean;
109
- }): SyncAdapter<S, M, E, MenuT> {
110
- const { multiplayerUrl, offlineStorageKey, debug } = params ?? {};
122
+ policyAuthContext?: PolicyAuthContext;
123
+ serverScripts?: LocalStorageSyncOptions["serverScripts"];
124
+ }): SyncAdapter<S, M, E, MenuT, I> {
125
+ const {
126
+ multiplayerUrl,
127
+ offlineStorageKey,
128
+ debug,
129
+ policyAuthContext,
130
+ serverScripts,
131
+ } = params ?? {};
111
132
 
112
133
  return isEmbeddedApp()
113
- ? parentFrameSync<S, M, E, MenuT>({ debug })
134
+ ? parentFrameSync<S, M, E, MenuT, I>({ debug, policyAuthContext })
114
135
  : multiplayerUrl
115
- ? webSocketSync<S, M, E, MenuT>({ url: multiplayerUrl, debug })
136
+ ? webSocketSync<S, M, E, MenuT, I>({ url: multiplayerUrl, debug })
116
137
  : offlineStorageKey
117
- ? localStorageSync<S, M, E, MenuT>({ key: offlineStorageKey })
118
- : stubSync<S, M, E, MenuT>();
138
+ ? localStorageSync<S, M, E, MenuT, I>({
139
+ key: offlineStorageKey,
140
+ ...(serverScripts ? { serverScripts } : {}),
141
+ })
142
+ : stubSync<S, M, E, MenuT, I>();
119
143
  }
120
144
 
121
145
  export type UseNoyaStateOptions<
@@ -123,7 +147,8 @@ export type UseNoyaStateOptions<
123
147
  M extends object = object,
124
148
  E extends object = object,
125
149
  MenuT extends string = string,
126
- > = UseMultiplayerStateOptions<S, M, E, MenuT> & {
150
+ I extends Record<string, any> = Record<string, unknown>,
151
+ > = UseMultiplayerStateOptions<S, M, E, MenuT, I> & {
127
152
  offlineStorageKey?: string;
128
153
  };
129
154
 
@@ -132,12 +157,14 @@ export type UseNoyaStateResult<
132
157
  M extends object = object,
133
158
  E extends object = object,
134
159
  MenuT extends string = string,
160
+ I extends Record<string, any> = Record<string, unknown>,
135
161
  > = [
136
- ReturnType<typeof useBindNoyaManager<S, M, E, MenuT>>[0],
137
- ReturnType<typeof useBindNoyaManager<S, M, E, MenuT>>[1],
138
- ReturnType<typeof useBindNoyaManager<S, M, E, MenuT>>[2] & {
162
+ ReturnType<typeof useBindNoyaManager<S, M, E, MenuT, I>>[0],
163
+ ReturnType<typeof useBindNoyaManager<S, M, E, MenuT, I>>[1],
164
+ ReturnType<typeof useBindNoyaManager<S, M, E, MenuT, I>>[2] & {
139
165
  theme: AppTheme;
140
166
  viewType: AppViewType;
167
+ isDemo: boolean;
141
168
  },
142
169
  ];
143
170
 
@@ -155,11 +182,13 @@ export function useNoyaStateInternal<
155
182
  M extends object = object,
156
183
  E extends object = object,
157
184
  MenuT extends string = string,
158
- O extends UseNoyaStateOptions<S, M, E, MenuT> = UseNoyaStateOptions<
185
+ I extends Record<string, any> = Record<string, unknown>,
186
+ O extends UseNoyaStateOptions<S, M, E, MenuT, I> = UseNoyaStateOptions<
159
187
  S,
160
188
  M,
161
189
  E,
162
- MenuT
190
+ MenuT,
191
+ I
163
192
  >,
164
193
  >(
165
194
  ...args:
@@ -174,7 +203,8 @@ export function useNoyaStateInternal<
174
203
  O["schema"] extends TSchema ? Static<O["schema"]> : S,
175
204
  M,
176
205
  E,
177
- MenuT
206
+ MenuT,
207
+ I
178
208
  > {
179
209
  const [initialState, options] =
180
210
  typeof args[0] === "object" &&
@@ -192,6 +222,9 @@ export function useNoyaStateInternal<
192
222
  initialState: noyaAppInitialState,
193
223
  theme,
194
224
  viewType,
225
+ userId,
226
+ isDemo,
227
+ access,
195
228
  },
196
229
  ] = useState(() => {
197
230
  return getAppData(initialState, options?.schema, {
@@ -199,19 +232,57 @@ export function useNoyaStateInternal<
199
232
  });
200
233
  });
201
234
 
202
- const sync = useMemo((): SyncAdapter<S, M, E, MenuT> => {
203
- return getSyncAdapter<S, M, E, MenuT>({
235
+ const basePolicyAuthContext = useMemo((): PolicyAuthContext | undefined => {
236
+ if (!access && userId === undefined) return undefined;
237
+
238
+ const context: PolicyAuthContext = {};
239
+
240
+ if (access) {
241
+ context.access = access;
242
+ }
243
+
244
+ if (userId !== undefined) {
245
+ context.id = userId ?? null;
246
+ }
247
+
248
+ return context;
249
+ }, [access, userId]);
250
+
251
+ const sync = useMemo((): SyncAdapter<S, M, E, MenuT, I> => {
252
+ return getSyncAdapter<S, M, E, MenuT, I>({
204
253
  multiplayerUrl,
205
254
  offlineStorageKey: options?.offlineStorageKey,
206
255
  debug: options?.debug,
256
+ policyAuthContext: basePolicyAuthContext,
207
257
  });
208
- }, [multiplayerUrl, options?.offlineStorageKey, options?.debug]);
258
+ }, [
259
+ multiplayerUrl,
260
+ options?.offlineStorageKey,
261
+ options?.debug,
262
+ basePolicyAuthContext,
263
+ ]);
209
264
 
210
265
  const [noyaManager] = useState(
211
- () => new NoyaManager<S, M, E, MenuT>(noyaAppInitialState as S, options)
266
+ () => new NoyaManager<S, M, E, MenuT, I>(noyaAppInitialState as S, options)
212
267
  );
213
268
 
214
- const result = useBindNoyaManager<S, M, E, MenuT>(noyaManager, {
269
+ useEffect(() => {
270
+ if (!userId) return;
271
+
272
+ noyaManager.sharedConnectionDataManager.setCurrentConnectionId(userId);
273
+ noyaManager.userManager.setCurrentConnectionId(userId);
274
+ noyaManager.userManager.setCurrentUserId(userId);
275
+ }, [noyaManager, userId]);
276
+
277
+ useEffect(() => {
278
+ if (!basePolicyAuthContext) return;
279
+
280
+ noyaManager.multiplayerStateManager.setPolicyAuthContext(
281
+ basePolicyAuthContext
282
+ );
283
+ }, [noyaManager, basePolicyAuthContext]);
284
+
285
+ const result = useBindNoyaManager<S, M, E, MenuT, I>(noyaManager, {
215
286
  inspector: options?.inspector ?? inspector,
216
287
  sync: options?.sync ?? sync,
217
288
  });
@@ -219,14 +290,15 @@ export function useNoyaStateInternal<
219
290
  const [, , extras] = result;
220
291
 
221
292
  const mergedExtras = useMemo(() => {
222
- return { ...extras, theme, viewType } as const;
223
- }, [extras, theme, viewType]);
293
+ return { ...extras, theme, viewType, isDemo } as const;
294
+ }, [extras, theme, viewType, isDemo]);
224
295
 
225
296
  return [result[0], result[1], mergedExtras] as UseNoyaStateResult<
226
297
  O["schema"] extends TSchema ? Static<O["schema"]> : S,
227
298
  M,
228
299
  E,
229
- MenuT
300
+ MenuT,
301
+ I
230
302
  >;
231
303
  }
232
304
 
package/src/singleton.tsx CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import type { LocalStorageSyncOptions } from "@noya-app/state-manager";
3
4
  import {
4
5
  NoyaManager,
5
6
  NoyaManagerOptions,
@@ -16,14 +17,16 @@ import {
16
17
  getSyncAdapter,
17
18
  } from "./noyaApp";
18
19
 
19
- let noyaManagerSingleton: NoyaManager<any, any, any, any> | undefined;
20
+ let noyaManagerSingleton: NoyaManager<any, any, any, any, any> | undefined;
20
21
  let noyaSingletonBindings: AnyNoyaSingletonBindings | undefined;
21
22
 
22
23
  export type AnyNoyaSingletonBindings = {
23
- sync?: SyncAdapter<any, any, any, any>;
24
+ sync?: SyncAdapter<any, any, any, any, any>;
24
25
  inspector?: boolean | StateInspectorOptions;
25
26
  theme: AppTheme;
26
27
  viewType: AppViewType;
28
+ userId?: string;
29
+ isDemo: boolean;
27
30
  };
28
31
 
29
32
  export type AnyNoyaSingletonOptions<
@@ -31,14 +34,16 @@ export type AnyNoyaSingletonOptions<
31
34
  M extends object,
32
35
  E extends object,
33
36
  MenuT extends string,
37
+ I extends Record<string, any> = Record<string, unknown>,
34
38
  > = NoyaManagerOptions<S, M> & {
35
39
  initialState?: S | (() => S);
36
- sync?: SyncAdapter<S, M, E, MenuT>;
40
+ sync?: SyncAdapter<S, M, E, MenuT, I>;
37
41
  inspector?: boolean | StateInspectorOptions;
38
42
  theme?: AppTheme;
39
43
  viewType?: AppViewType;
40
44
  offlineStorageKey?: string;
41
45
  multiplayerUrl?: string;
46
+ serverScripts?: LocalStorageSyncOptions["serverScripts"];
42
47
  };
43
48
 
44
49
  export function initializeNoyaSingleton<
@@ -46,7 +51,8 @@ export function initializeNoyaSingleton<
46
51
  M extends object,
47
52
  E extends object,
48
53
  MenuT extends string,
49
- >(options?: AnyNoyaSingletonOptions<S, M, E, MenuT>) {
54
+ I extends Record<string, any> = Record<string, unknown>,
55
+ >(options?: AnyNoyaSingletonOptions<S, M, E, MenuT, I>) {
50
56
  if (noyaManagerSingleton) {
51
57
  throw new Error(
52
58
  "Noya has already been initialized. Call `useNoyaState` instead."
@@ -61,8 +67,9 @@ export function initializeNoyaSingleton<
61
67
  viewType,
62
68
  offlineStorageKey,
63
69
  multiplayerUrl,
70
+ serverScripts,
64
71
  ...managerOptions
65
- } = (options ?? {}) as AnyNoyaSingletonOptions<S, M, E, MenuT>;
72
+ } = (options ?? {}) as AnyNoyaSingletonOptions<S, M, E, MenuT, I>;
66
73
 
67
74
  const appData = getAppData<S>(
68
75
  (initialState ?? null) as S | (() => S),
@@ -70,17 +77,37 @@ export function initializeNoyaSingleton<
70
77
  { overrideExistingState: managerOptions?.overrideExistingState }
71
78
  );
72
79
 
73
- noyaManagerSingleton = new NoyaManager<S, M, E, MenuT>(
80
+ const policyAuthContext =
81
+ appData.access || appData.userId !== undefined
82
+ ? {
83
+ ...(appData.access ? { access: appData.access } : {}),
84
+ ...(appData.userId !== undefined
85
+ ? { id: appData.userId ?? null }
86
+ : {}),
87
+ }
88
+ : undefined;
89
+
90
+ noyaManagerSingleton = new NoyaManager<S, M, E, MenuT, I>(
74
91
  appData.initialState as S,
75
92
  managerOptions
76
93
  );
77
94
 
95
+ if (appData.userId) {
96
+ noyaManagerSingleton.sharedConnectionDataManager.setCurrentConnectionId(
97
+ appData.userId
98
+ );
99
+ noyaManagerSingleton.userManager.setCurrentConnectionId(appData.userId);
100
+ noyaManagerSingleton.userManager.setCurrentUserId(appData.userId);
101
+ }
102
+
78
103
  const resolvedSync =
79
104
  sync ??
80
- getSyncAdapter<S, M, E, MenuT>({
105
+ getSyncAdapter<S, M, E, MenuT, I>({
81
106
  multiplayerUrl: multiplayerUrl ?? appData.multiplayerUrl,
82
107
  offlineStorageKey,
83
108
  debug: managerOptions?.debug,
109
+ serverScripts,
110
+ policyAuthContext,
84
111
  });
85
112
 
86
113
  // Activate sync immediately for the singleton
@@ -108,6 +135,9 @@ export function initializeNoyaSingleton<
108
135
  anchor={
109
136
  resolvedInspector === true ? "bottom right" : resolvedInspector.anchor
110
137
  }
138
+ advanced={
139
+ resolvedInspector === true ? false : resolvedInspector.advanced
140
+ }
111
141
  />
112
142
  );
113
143
  }
@@ -117,10 +147,18 @@ export function initializeNoyaSingleton<
117
147
  inspector: resolvedInspector,
118
148
  theme: resolvedTheme,
119
149
  viewType: resolvedViewType,
150
+ userId: appData.userId,
151
+ isDemo: appData.isDemo,
120
152
  };
121
153
  }
122
154
 
123
- export function getNoyaManagerSingleton() {
155
+ export function getNoyaManagerSingleton(): NoyaManager<
156
+ any,
157
+ any,
158
+ any,
159
+ any,
160
+ any
161
+ > {
124
162
  if (!noyaManagerSingleton || !noyaSingletonBindings) {
125
163
  throw new Error(
126
164
  "Noya has not been initialized. Call initializeNoya once before using Noya hooks."