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