@four-leaf-studios/rl-overlay 1.0.2 → 1.0.3
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/dist/index.cjs.js +147 -97
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.esm.js +149 -99
- package/dist/index.esm.js.map +1 -1
- package/package.json +2 -2
- package/test-overlay/package.json +27 -27
package/dist/index.cjs.js
CHANGED
|
@@ -22,62 +22,160 @@ function _interopNamespaceDefault(e) {
|
|
|
22
22
|
|
|
23
23
|
var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
// useRocketLeagueSocket.ts
|
|
26
|
+
const reducer = (state, action) => ({
|
|
27
|
+
...state,
|
|
28
|
+
[action.type]: action.payload,
|
|
29
|
+
});
|
|
30
|
+
const normalizeEvent = (raw) => {
|
|
31
|
+
if (typeof raw === "object" && raw !== null) {
|
|
32
|
+
// { event: "name", data: {...} }
|
|
33
|
+
if (typeof raw.event === "string" && "data" in raw) {
|
|
34
|
+
return [[raw.event, raw.data]];
|
|
35
|
+
}
|
|
36
|
+
// { "evt1": {...}, "evt2": {...} }
|
|
37
|
+
return Object.entries(raw);
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
};
|
|
41
|
+
function useRocketLeagueSocket(url = "ws://localhost:49122", options) {
|
|
42
|
+
const { maxRetries = 5, heartbeatIntervalMs = 30_000 } = options || {};
|
|
43
|
+
// Reducer state holds the map of eventName → payload
|
|
44
|
+
const [events, dispatch] = React.useReducer(reducer, {});
|
|
45
|
+
const [readyState, setReadyState] = React.useState(WebSocket.CLOSED);
|
|
46
|
+
const [error, setError] = React.useState(null);
|
|
47
|
+
// Refs for the socket, retry count, and heartbeat timer
|
|
48
|
+
const socketRef = React.useRef(null);
|
|
49
|
+
const retryRef = React.useRef(0);
|
|
50
|
+
const heartbeatRef = React.useRef(undefined);
|
|
51
|
+
// Expose a send() helper
|
|
52
|
+
const send = React.useCallback((event, data) => {
|
|
53
|
+
const sock = socketRef.current;
|
|
54
|
+
if (sock?.readyState === WebSocket.OPEN) {
|
|
55
|
+
sock.send(JSON.stringify({ event, data }));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.warn("Cannot send, socket not open:", event);
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
30
61
|
React.useEffect(() => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
let isMounted = true;
|
|
63
|
+
const connect = () => {
|
|
64
|
+
const ws = new WebSocket(url);
|
|
65
|
+
socketRef.current = ws;
|
|
66
|
+
setReadyState(ws.readyState);
|
|
67
|
+
ws.onopen = () => {
|
|
68
|
+
if (!isMounted)
|
|
69
|
+
return;
|
|
70
|
+
retryRef.current = 0;
|
|
71
|
+
setReadyState(ws.readyState);
|
|
72
|
+
// Start heartbeat to keep alive
|
|
73
|
+
heartbeatRef.current = window.setInterval(() => {
|
|
74
|
+
ws.send(JSON.stringify({ event: "ping" }));
|
|
75
|
+
}, heartbeatIntervalMs);
|
|
76
|
+
};
|
|
77
|
+
ws.onmessage = (e) => {
|
|
78
|
+
if (!isMounted)
|
|
79
|
+
return;
|
|
80
|
+
let raw;
|
|
81
|
+
try {
|
|
82
|
+
raw = JSON.parse(e.data);
|
|
45
83
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
84
|
+
catch {
|
|
85
|
+
console.error("Invalid JSON from server:", e.data);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// ignore pong replies
|
|
89
|
+
if (raw.event === "pong")
|
|
90
|
+
return;
|
|
91
|
+
// dispatch each normalized event
|
|
92
|
+
for (const [evt, payload] of normalizeEvent(raw)) {
|
|
93
|
+
dispatch({ type: evt, payload });
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
ws.onerror = (ev) => {
|
|
97
|
+
if (!isMounted)
|
|
98
|
+
return;
|
|
99
|
+
console.error("WebSocket error", ev);
|
|
100
|
+
setError(ev);
|
|
101
|
+
};
|
|
102
|
+
ws.onclose = () => {
|
|
103
|
+
if (!isMounted)
|
|
104
|
+
return;
|
|
105
|
+
setReadyState(WebSocket.CLOSED);
|
|
106
|
+
if (heartbeatRef.current !== undefined) {
|
|
107
|
+
clearInterval(heartbeatRef.current);
|
|
108
|
+
}
|
|
109
|
+
// attempt reconnect
|
|
110
|
+
if (retryRef.current < maxRetries) {
|
|
111
|
+
const backoff = 2 ** retryRef.current * 1000;
|
|
112
|
+
retryRef.current += 1;
|
|
113
|
+
setTimeout(connect, backoff);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.warn("Reached max WebSocket retries");
|
|
117
|
+
}
|
|
118
|
+
};
|
|
50
119
|
};
|
|
120
|
+
connect();
|
|
51
121
|
return () => {
|
|
52
|
-
|
|
122
|
+
isMounted = false;
|
|
123
|
+
if (heartbeatRef.current !== undefined) {
|
|
124
|
+
clearInterval(heartbeatRef.current);
|
|
125
|
+
}
|
|
126
|
+
socketRef.current?.close();
|
|
53
127
|
};
|
|
54
|
-
}, [url]);
|
|
55
|
-
|
|
128
|
+
}, [url, maxRetries, heartbeatIntervalMs]);
|
|
129
|
+
return { events, readyState, error, send };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const RLContext = React.createContext(null);
|
|
133
|
+
const RLProvider = ({ url = "ws://localhost:49122", children }) => {
|
|
134
|
+
// 1) pull in every event & helpers from the single socket hook
|
|
135
|
+
const { events: allEvents, send, readyState, error, } = useRocketLeagueSocket(url);
|
|
136
|
+
// 2) Use a loose-typed ref to avoid union-vs-intersection complaints
|
|
137
|
+
const dataRef = React.useRef({});
|
|
138
|
+
const subsRef = React.useRef({});
|
|
139
|
+
// 3) Sync incoming allEvents into dataRef and notify subscribers
|
|
140
|
+
React.useEffect(() => {
|
|
141
|
+
// Force evt to be keyof PayloadStorage for type-narrowing
|
|
142
|
+
const keys = Object.keys(allEvents);
|
|
143
|
+
keys.forEach((evt) => {
|
|
144
|
+
const payload = allEvents[evt];
|
|
145
|
+
// write into our record (string index)
|
|
146
|
+
dataRef.current[evt] = payload;
|
|
147
|
+
// notify anyone subscribed to this key
|
|
148
|
+
(subsRef.current[evt] || []).forEach((cb) => cb());
|
|
149
|
+
});
|
|
150
|
+
}, [allEvents]);
|
|
151
|
+
// 4) Build the same Store API, casting when reading back
|
|
56
152
|
const store = React.useMemo(() => ({
|
|
57
|
-
getSnapshot: (eventName) =>
|
|
153
|
+
getSnapshot: (eventName) =>
|
|
154
|
+
// cast from any back to the correct PayloadStorage type
|
|
155
|
+
dataRef.current[eventName],
|
|
58
156
|
subscribe: (eventName, callback) => {
|
|
59
|
-
const
|
|
60
|
-
|
|
157
|
+
const arr = (subsRef.current[eventName] ??= []);
|
|
158
|
+
arr.push(callback);
|
|
61
159
|
return () => {
|
|
62
|
-
|
|
160
|
+
subsRef.current[eventName] = arr.filter((c) => c !== callback);
|
|
63
161
|
};
|
|
64
162
|
},
|
|
65
|
-
|
|
163
|
+
send,
|
|
164
|
+
readyState,
|
|
165
|
+
error,
|
|
166
|
+
}), [send, readyState, error]);
|
|
66
167
|
return jsxRuntime.jsx(RLContext.Provider, { value: store, children: children });
|
|
67
168
|
};
|
|
68
169
|
/**
|
|
69
|
-
* Subscribe to the full
|
|
170
|
+
* Subscribe to the full payload of an event.
|
|
70
171
|
*/
|
|
71
172
|
function useEvent(eventName) {
|
|
72
173
|
const store = React.useContext(RLContext);
|
|
73
174
|
if (!store)
|
|
74
175
|
throw new Error("useEvent must be inside <RLProvider>");
|
|
75
|
-
// 2) (optional) explicitly tell TS what snapshot type is
|
|
76
176
|
return React.useSyncExternalStore((cb) => store.subscribe(eventName, cb), () => store.getSnapshot(eventName));
|
|
77
177
|
}
|
|
78
|
-
/**
|
|
79
|
-
* Tiny deep-equal for JavaScript values
|
|
80
|
-
*/
|
|
178
|
+
/** Tiny deep-equal for selector comparisons */
|
|
81
179
|
function deepEqual(a, b) {
|
|
82
180
|
if (Object.is(a, b))
|
|
83
181
|
return true;
|
|
@@ -87,34 +185,21 @@ function deepEqual(a, b) {
|
|
|
87
185
|
b === null) {
|
|
88
186
|
return false;
|
|
89
187
|
}
|
|
90
|
-
const aKeys = Object.keys(a);
|
|
91
|
-
const bKeys = Object.keys(b);
|
|
188
|
+
const aKeys = Object.keys(a), bKeys = Object.keys(b);
|
|
92
189
|
if (aKeys.length !== bKeys.length)
|
|
93
190
|
return false;
|
|
94
|
-
|
|
95
|
-
if (!deepEqual(a[key], b[key]))
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
return true;
|
|
191
|
+
return aKeys.every((key) => deepEqual(a[key], b[key]));
|
|
99
192
|
}
|
|
100
193
|
/**
|
|
101
|
-
* Subscribe to a selected slice of the event payload.
|
|
102
|
-
* Uses a built-in deep-equality check by default (no lodash).
|
|
194
|
+
* Subscribe to a selected slice of the event payload. Re-renders only on actual changes.
|
|
103
195
|
*/
|
|
104
|
-
function useEventSelector(
|
|
105
|
-
/** Event key to subscribe to */
|
|
106
|
-
eventName,
|
|
107
|
-
/** Selector that picks a slice of the payload */
|
|
108
|
-
selector,
|
|
109
|
-
/** Equality function to compare selector results; defaults to deepEqual */
|
|
110
|
-
isEqual = deepEqual) {
|
|
196
|
+
function useEventSelector(eventName, selector, isEqual = deepEqual) {
|
|
111
197
|
const store = React.useContext(RLContext);
|
|
112
198
|
const lastRef = React.useRef({});
|
|
113
199
|
const subscribe = React.useCallback((cb) => store.subscribe(eventName, cb), [eventName, store]);
|
|
114
200
|
const getSnapshot = React.useCallback(() => {
|
|
115
201
|
const raw = store.getSnapshot(eventName);
|
|
116
202
|
const next = selector(raw);
|
|
117
|
-
// deep-equal by default
|
|
118
203
|
if (lastRef.current.val !== undefined &&
|
|
119
204
|
isEqual(next, lastRef.current.val)) {
|
|
120
205
|
return lastRef.current.val;
|
|
@@ -125,51 +210,16 @@ isEqual = deepEqual) {
|
|
|
125
210
|
return React.useSyncExternalStore(subscribe, getSnapshot);
|
|
126
211
|
}
|
|
127
212
|
|
|
128
|
-
const
|
|
129
|
-
...state,
|
|
130
|
-
[action.type]: action.payload,
|
|
131
|
-
});
|
|
132
|
-
const normalizeEvent = (raw) => {
|
|
133
|
-
if (typeof raw === "object") {
|
|
134
|
-
// Format 1: { event: "game:update_state", data: { ... } }
|
|
135
|
-
if (typeof raw.event === "string" && "data" in raw) {
|
|
136
|
-
return [[raw.event, raw.data]];
|
|
137
|
-
}
|
|
138
|
-
// Format 2: { "game:update_state": { ... }, "game:goal_scored": { ... } }
|
|
139
|
-
return Object.entries(raw);
|
|
140
|
-
}
|
|
141
|
-
return [];
|
|
142
|
-
};
|
|
143
|
-
const useRocketLeagueSocket = (url = "ws://localhost:49122") => {
|
|
144
|
-
const [state, dispatch] = React.useReducer(reducer, {});
|
|
145
|
-
React.useEffect(() => {
|
|
146
|
-
const socket = new WebSocket(url);
|
|
147
|
-
socket.onmessage = (event) => {
|
|
148
|
-
try {
|
|
149
|
-
const rawData = JSON.parse(event.data);
|
|
150
|
-
const events = normalizeEvent(rawData);
|
|
151
|
-
events.forEach(([eventName, payload]) => {
|
|
152
|
-
dispatch({ type: eventName, payload });
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
catch (err) {
|
|
156
|
-
console.error("WebSocket error: Invalid JSON", err);
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
return () => socket.close();
|
|
160
|
-
}, [url]);
|
|
161
|
-
return state;
|
|
162
|
-
};
|
|
163
|
-
|
|
213
|
+
const STATE_LABELS = ["CONNECTING", "OPEN", "CLOSING", "CLOSED"];
|
|
164
214
|
const WebsocketData = () => {
|
|
165
|
-
const
|
|
166
|
-
return (jsxRuntime.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
215
|
+
const { events, readyState, error } = useRocketLeagueSocket();
|
|
216
|
+
return (jsxRuntime.jsxs("div", { style: { fontFamily: "monospace", padding: "1rem" }, children: [jsxRuntime.jsx("h1", { children: "Rocket League Live Events" }), jsxRuntime.jsxs("p", { children: [jsxRuntime.jsx("strong", { children: "Connection:" }), " ", STATE_LABELS[readyState] ?? readyState] }), error && (jsxRuntime.jsxs("p", { style: { color: "red" }, children: [jsxRuntime.jsx("strong", { children: "Error:" }), " ", String(error)] })), Object.entries(events).map(([event, payload]) => (jsxRuntime.jsxs("div", { style: {
|
|
217
|
+
border: "1px solid #ccc",
|
|
218
|
+
marginBottom: "1rem",
|
|
219
|
+
padding: "0.5rem",
|
|
220
|
+
borderRadius: "4px",
|
|
221
|
+
backgroundColor: "#f9f9f9",
|
|
222
|
+
}, children: [jsxRuntime.jsx("h2", { children: event }), jsxRuntime.jsx("pre", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word" }, children: JSON.stringify(payload, null, 2) })] }, event))), Object.keys(events).length === 0 && jsxRuntime.jsx("p", { children: "No events received yet." })] }));
|
|
173
223
|
};
|
|
174
224
|
|
|
175
225
|
var BroadcastContext = React.createContext(undefined);
|