@noya-app/noya-multiplayer-react 0.1.27 → 0.1.29

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,107 @@
1
+ import { expect, it } from "bun:test";
2
+ import { Type } from "..";
3
+ import { getAppData } from "../noyaApp";
4
+
5
+ it("gets AppData with no data", () => {
6
+ const initialState = { foo: "bar" };
7
+ const data = getAppData(initialState, undefined, {
8
+ window: { location: { hash: "" } },
9
+ });
10
+
11
+ expect(data).toEqual({
12
+ theme: "light",
13
+ viewType: "editable",
14
+ initialState,
15
+ inspector: false,
16
+ });
17
+ });
18
+
19
+ it("gets AppData with data param", () => {
20
+ const dataParam = JSON.stringify({
21
+ theme: "light",
22
+ viewType: "editable",
23
+ initialState: { foo: "bar" },
24
+ inspector: false,
25
+ });
26
+
27
+ const data = getAppData(null as unknown, undefined, {
28
+ window: {
29
+ location: {
30
+ hash: `#data=${dataParam}`,
31
+ },
32
+ },
33
+ });
34
+
35
+ expect(data).toEqual({
36
+ theme: "light",
37
+ viewType: "editable",
38
+ initialState: { foo: "bar" },
39
+ inspector: false,
40
+ });
41
+ });
42
+
43
+ it("uses initial state if data doesn't parse", () => {
44
+ const data = getAppData({ foo: "bar" }, undefined, {
45
+ window: { location: { hash: "#data=not-json" } },
46
+ });
47
+
48
+ expect(data).toEqual({
49
+ theme: "light",
50
+ viewType: "editable",
51
+ initialState: { foo: "bar" },
52
+ inspector: false,
53
+ });
54
+ });
55
+
56
+ it("gets AppData with schema", () => {
57
+ const schema = Type.Object({
58
+ foo: Type.String({ default: "bar" }),
59
+ });
60
+
61
+ const data = getAppData(null as unknown, schema, {
62
+ window: { location: { hash: "" } },
63
+ });
64
+
65
+ expect(data).toEqual({
66
+ theme: "light",
67
+ viewType: "editable",
68
+ initialState: { foo: "bar" },
69
+ inspector: false,
70
+ });
71
+ });
72
+
73
+ // it("enforces schema on data param", () => {
74
+ // const schema = Type.Object({
75
+ // foo: Type.String({ default: "bar" }),
76
+ // });
77
+
78
+ // const data = getAppData({ foo: "baz" }, schema, {
79
+ // window: {
80
+ // location: {
81
+ // hash: `#data=${JSON.stringify({ a: 123 })}`,
82
+ // },
83
+ // },
84
+ // });
85
+
86
+ // expect(data).toEqual({
87
+ // theme: "light",
88
+ // viewType: "editable",
89
+ // initialState: { foo: "bar" },
90
+ // inspector: false,
91
+ // });
92
+ // });
93
+
94
+ it("gets AppData with function initial state", () => {
95
+ const initialState = () => ({ foo: "bar" });
96
+
97
+ const data = getAppData(initialState, undefined, {
98
+ window: { location: { hash: "" } },
99
+ });
100
+
101
+ expect(data).toEqual({
102
+ theme: "light",
103
+ viewType: "editable",
104
+ initialState: { foo: "bar" },
105
+ inspector: false,
106
+ });
107
+ });
package/src/noyaApp.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable no-restricted-globals */
2
2
  import {
3
3
  createOrCastValue,
4
+ findAllDefs,
4
5
  localStorageSync,
5
6
  MultiplayerUser,
6
7
  SyncAdapter,
@@ -34,46 +35,56 @@ export function createDefaultAppData<State>(
34
35
  };
35
36
  }
36
37
 
37
- export function getAppData<State>(): AppData<State> | undefined;
38
- export function getAppData<State>(initialState: State): AppData<State>;
39
- export function getAppData<State>(
40
- initialState?: State
41
- ): AppData<State> | undefined {
38
+ export type GetAppDataOptions = {
39
+ window?: { location: { hash: string } };
40
+ };
41
+
42
+ export function enforceSchema<State>(
43
+ initialState: State,
44
+ schema?: TSchema
45
+ ): State {
46
+ return schema
47
+ ? (createOrCastValue({
48
+ schema,
49
+ value: initialState,
50
+ defs: findAllDefs(schema),
51
+ }) as State)
52
+ : initialState;
53
+ }
54
+
55
+ export function parseAppDataParameter(options?: GetAppDataOptions) {
56
+ const window = options?.window ?? globalThis;
57
+ const urlHash = (window.location.hash || "").replace(/^#/, "");
58
+ const params = new URLSearchParams(urlHash);
59
+ const data = params.get("data");
60
+ if (!data) return null;
42
61
  try {
43
- const urlHash = (window.location.hash || "").replace(/^#/, "");
44
- const params = new URLSearchParams(urlHash);
45
- const data = params.get("data");
46
- if (!data) {
47
- return initialState ? createDefaultAppData(initialState) : undefined;
48
- }
49
- const parsed = JSON.parse(data);
50
- if (!parsed.initialState) {
51
- parsed.initialState = initialState;
52
- }
53
- return parsed;
62
+ return JSON.parse(data);
54
63
  } catch (e) {
55
- return initialState ? createDefaultAppData(initialState) : undefined;
64
+ return null;
56
65
  }
57
66
  }
58
67
 
59
- type AppDataOptions = {
60
- schema?: TSchema;
61
- };
68
+ export function getAppData<State>(
69
+ initialState: State | (() => State),
70
+ schema?: TSchema,
71
+ options?: GetAppDataOptions
72
+ ): AppData<State> {
73
+ const resolvedInitialState =
74
+ initialState instanceof Function ? initialState() : initialState;
62
75
 
63
- /**
64
- * Get data from host app. Fall back to default data.
65
- */
66
- export function useAppData<S>(
67
- initialState: S,
68
- { schema }: AppDataOptions = {}
69
- ) {
70
- const appData = useMemo(() => {
71
- return getAppData(
72
- schema ? (createOrCastValue(schema, initialState) as S) : initialState
73
- );
74
- }, [initialState, schema]);
76
+ try {
77
+ const appData: AppData<unknown> =
78
+ parseAppDataParameter(options) ??
79
+ createDefaultAppData(resolvedInitialState);
75
80
 
76
- return appData;
81
+ return {
82
+ ...appData,
83
+ initialState: enforceSchema(appData.initialState, schema) as State,
84
+ };
85
+ } catch (e) {
86
+ return createDefaultAppData(enforceSchema(resolvedInitialState, schema));
87
+ }
77
88
  }
78
89
 
79
90
  export type IframeToHostMessage =
@@ -148,17 +159,17 @@ export function useNoyaAppState<S, M = void, E = void>(
148
159
  localStorageKey?: string;
149
160
  }
150
161
  ) {
151
- // Resolve state to a value
152
- const [state] = useState(initialState);
153
-
154
- const {
155
- multiplayerUrl,
156
- inspector,
157
- initialState: noyaAppInitialState,
158
- theme,
159
- viewType,
160
- } = useAppData(state, {
161
- schema: options.schema,
162
+ // Keep these stable after initial render
163
+ const [
164
+ {
165
+ multiplayerUrl,
166
+ inspector,
167
+ initialState: noyaAppInitialState,
168
+ theme,
169
+ viewType,
170
+ },
171
+ ] = useState(() => {
172
+ return getAppData(initialState, options.schema);
162
173
  });
163
174
 
164
175
  const sync = useMemo((): SyncAdapter<S, M, E> => {