@noya-app/noya-multiplayer-react 0.1.21 → 0.1.23
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 +9 -9
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +67 -14
- package/dist/index.d.ts +67 -14
- package/dist/index.js +133 -8
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +130 -8
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/hooks.ts +26 -20
- package/src/index.ts +1 -0
- package/src/inspector/StateInspector.tsx +25 -5
- package/src/noyaApp.ts +185 -0
package/src/noyaApp.ts
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/* eslint-disable no-restricted-globals */
|
|
2
|
+
import {
|
|
3
|
+
createValue,
|
|
4
|
+
localStorageSync,
|
|
5
|
+
MultiplayerUser,
|
|
6
|
+
SyncAdapter,
|
|
7
|
+
TSchema,
|
|
8
|
+
webSocketSync,
|
|
9
|
+
} from "@noya-app/state-manager";
|
|
10
|
+
import { useEffect, useMemo, useState } from "react";
|
|
11
|
+
import { useMultiplayerState, UseMultiplayerStateOptions } from "./hooks";
|
|
12
|
+
import { StateInspectorOptions } from "./inspector/StateInspector";
|
|
13
|
+
|
|
14
|
+
export type AppViewType = "editable" | "readOnly" | "preview";
|
|
15
|
+
|
|
16
|
+
export type AppTheme = "light" | "dark";
|
|
17
|
+
|
|
18
|
+
export type AppData<State> = {
|
|
19
|
+
multiplayerUrl?: string;
|
|
20
|
+
theme: AppTheme;
|
|
21
|
+
viewType: AppViewType;
|
|
22
|
+
initialState: State;
|
|
23
|
+
inspector: boolean | StateInspectorOptions;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function createDefaultAppData<State>(
|
|
27
|
+
initialState: State
|
|
28
|
+
): AppData<State> {
|
|
29
|
+
return {
|
|
30
|
+
theme: "light",
|
|
31
|
+
viewType: "editable",
|
|
32
|
+
initialState,
|
|
33
|
+
inspector: false,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
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 {
|
|
42
|
+
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;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return initialState ? createDefaultAppData(initialState) : undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type AppDataOptions = {
|
|
60
|
+
schema?: TSchema;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Get data from host app. Fall back to default data.
|
|
65
|
+
*/
|
|
66
|
+
export function useAppData<S extends object>(
|
|
67
|
+
initialState: S,
|
|
68
|
+
{ schema }: AppDataOptions = {}
|
|
69
|
+
) {
|
|
70
|
+
const appData = useMemo(() => {
|
|
71
|
+
return getAppData(
|
|
72
|
+
schema ? createValue(schema, initialState) : initialState
|
|
73
|
+
);
|
|
74
|
+
}, [initialState, schema]);
|
|
75
|
+
|
|
76
|
+
return appData;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type IframeToHostMessage =
|
|
80
|
+
| {
|
|
81
|
+
type: "application.setMenuItems";
|
|
82
|
+
payload: {
|
|
83
|
+
menuItems: any[];
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
type: "multiplayer.setUsers";
|
|
88
|
+
payload: {
|
|
89
|
+
users: any[];
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export type HostToIframeMessage = {
|
|
94
|
+
type: "application.selectMenuItem";
|
|
95
|
+
payload: {
|
|
96
|
+
value: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export function useBroadcastConnectedUsers(connectedUsers: MultiplayerUser[]) {
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!isEmbeddedApp()) return;
|
|
103
|
+
|
|
104
|
+
const message: IframeToHostMessage = {
|
|
105
|
+
type: "multiplayer.setUsers",
|
|
106
|
+
payload: { users: connectedUsers },
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
parent?.postMessage(message, "*");
|
|
110
|
+
}, [connectedUsers]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function useBroadcastMenuItems<T>(
|
|
114
|
+
menuItems: any[],
|
|
115
|
+
handleSelectMenuItem: (value: T) => void
|
|
116
|
+
) {
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
if (!isEmbeddedApp()) return;
|
|
119
|
+
|
|
120
|
+
const message: IframeToHostMessage = {
|
|
121
|
+
type: "application.setMenuItems",
|
|
122
|
+
payload: { menuItems },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
parent?.postMessage(message, "*");
|
|
126
|
+
|
|
127
|
+
const listener = (event: MessageEvent<HostToIframeMessage>) => {
|
|
128
|
+
if (event.data.type === "application.selectMenuItem") {
|
|
129
|
+
handleSelectMenuItem(event.data.payload.value as T);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
window.addEventListener("message", listener);
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
window.removeEventListener("message", listener);
|
|
137
|
+
};
|
|
138
|
+
}, [handleSelectMenuItem, menuItems]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function isEmbeddedApp() {
|
|
142
|
+
return window.self !== window.top;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function useNoyaAppState<S extends object, M = void, E = void>(
|
|
146
|
+
initialState: S | (() => S),
|
|
147
|
+
options: UseMultiplayerStateOptions<S, M, E> & {
|
|
148
|
+
localStorageKey?: string;
|
|
149
|
+
}
|
|
150
|
+
) {
|
|
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
|
+
});
|
|
163
|
+
|
|
164
|
+
const sync = useMemo((): SyncAdapter<S, M, E> => {
|
|
165
|
+
return multiplayerUrl
|
|
166
|
+
? webSocketSync(multiplayerUrl)
|
|
167
|
+
: localStorageSync({ key: options.localStorageKey || "noya-app" });
|
|
168
|
+
}, [multiplayerUrl, options.localStorageKey]);
|
|
169
|
+
|
|
170
|
+
const result = useMultiplayerState(noyaAppInitialState, {
|
|
171
|
+
...options,
|
|
172
|
+
inspector: options.inspector ?? inspector,
|
|
173
|
+
sync,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const [, , extras] = result;
|
|
177
|
+
|
|
178
|
+
useBroadcastConnectedUsers(extras.connectedUsers);
|
|
179
|
+
|
|
180
|
+
const mergedExtras = useMemo(() => {
|
|
181
|
+
return { ...extras, theme, viewType } as const;
|
|
182
|
+
}, [extras, theme, viewType]);
|
|
183
|
+
|
|
184
|
+
return [result[0], result[1], mergedExtras] as const;
|
|
185
|
+
}
|