preact-missing-hooks 3.1.0 → 4.1.0
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/.husky/pre-commit +1 -0
- package/.husky/pre-push +1 -0
- package/.prettierignore +3 -0
- package/.prettierrc +6 -0
- package/Readme.md +333 -137
- package/dist/entry.cjs +21 -0
- package/dist/entry.js +2 -0
- package/dist/entry.js.map +1 -0
- package/dist/entry.modern.mjs +2 -0
- package/dist/entry.modern.mjs.map +1 -0
- package/dist/entry.module.js +2 -0
- package/dist/entry.module.js.map +1 -0
- package/dist/entry.umd.js +2 -0
- package/dist/entry.umd.js.map +1 -0
- package/dist/index.d.ts +14 -13
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.mjs +2 -0
- package/dist/index.modern.mjs.map +1 -0
- package/dist/index.module.js +1 -1
- package/dist/index.module.js.map +1 -1
- package/dist/index.umd.js +1 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/indexedDB/dbController.d.ts +2 -2
- package/dist/indexedDB/index.d.ts +6 -6
- package/dist/indexedDB/openDB.d.ts +1 -1
- package/dist/indexedDB/tableController.d.ts +1 -1
- package/dist/indexedDB/types.d.ts +1 -2
- package/dist/react.js +1 -0
- package/dist/react.modern.mjs +1 -0
- package/dist/react.module.js +1 -0
- package/dist/react.umd.js +1 -0
- package/dist/useEventBus.d.ts +1 -1
- package/dist/useIndexedDB.d.ts +3 -3
- package/dist/useLLMMetadata.d.ts +71 -0
- package/dist/useMutationObserver.d.ts +1 -1
- package/dist/useNetworkState.d.ts +3 -3
- package/dist/usePreferredTheme.d.ts +1 -1
- package/dist/useRageClick.d.ts +1 -1
- package/dist/useThreadedWorker.d.ts +1 -1
- package/dist/useTransition.d.ts +4 -1
- package/dist/useWorkerNotifications.d.ts +1 -1
- package/dist/useWrappedChildren.d.ts +3 -3
- package/docs/README.md +111 -0
- package/docs/index.html +58 -20
- package/docs/main.js +49 -0
- package/eslint.config.mjs +10 -0
- package/package.json +60 -6
- package/scripts/generate-entry.cjs +34 -0
- package/src/index.ts +14 -13
- package/src/indexedDB/dbController.ts +101 -92
- package/src/indexedDB/index.ts +16 -11
- package/src/indexedDB/openDB.ts +49 -49
- package/src/indexedDB/requestToPromise.ts +17 -16
- package/src/indexedDB/tableController.ts +331 -257
- package/src/indexedDB/types.ts +35 -35
- package/src/useClipboard.ts +99 -97
- package/src/useEventBus.ts +39 -36
- package/src/useIndexedDB.ts +111 -111
- package/src/useLLMMetadata.ts +418 -0
- package/src/useMutationObserver.ts +26 -26
- package/src/useNetworkState.ts +124 -122
- package/src/usePreferredTheme.ts +68 -68
- package/src/useRageClick.ts +103 -103
- package/src/useThreadedWorker.ts +165 -165
- package/src/useTransition.ts +22 -19
- package/src/useWasmCompute.ts +209 -204
- package/src/useWebRTCIP.ts +181 -176
- package/src/useWorkerNotifications.ts +28 -20
- package/src/useWrappedChildren.ts +72 -58
- package/tests/preact-as-react.ts +5 -0
- package/tests/react-adapter.tsx +12 -0
- package/tests/setup-react.ts +4 -0
- package/tests/useClipboard.test.tsx +4 -2
- package/tests/useLLMMetadata.test.tsx +149 -0
- package/tests/useThreadedWorker.test.tsx +3 -1
- package/tests/useWasmCompute.test.tsx +1 -1
- package/tests/useWebRTCIP.test.tsx +3 -1
- package/vite.config.ts +11 -4
- package/vitest.config.preact.ts +21 -0
- package/vitest.config.react.ts +36 -0
- package/vitest.workspace.ts +6 -0
package/src/useWebRTCIP.ts
CHANGED
|
@@ -1,176 +1,181 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useWebRTCIP – detect local/public IPs via WebRTC ICE candidates and STUN.
|
|
3
|
-
* Not highly reliable; use as first-priority hint and fall back to a public IP API (e.g. ipapi.co) if needed.
|
|
4
|
-
* @module useWebRTCIP
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { useEffect, useState, useRef } from
|
|
8
|
-
|
|
9
|
-
/** IPv4 regex for ICE candidate strings (captures dotted-decimal). */
|
|
10
|
-
const IPV4_REGEX =
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* @
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
|
|
1
|
+
/**
|
|
2
|
+
* useWebRTCIP – detect local/public IPs via WebRTC ICE candidates and STUN.
|
|
3
|
+
* Not highly reliable; use as first-priority hint and fall back to a public IP API (e.g. ipapi.co) if needed.
|
|
4
|
+
* @module useWebRTCIP
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState, useRef } from "preact/hooks";
|
|
8
|
+
|
|
9
|
+
/** IPv4 regex for ICE candidate strings (captures dotted-decimal). */
|
|
10
|
+
const IPV4_REGEX =
|
|
11
|
+
/\b(?:25[0-5]|2[0-4]\d|1?\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1?\d{1,2})){3}\b/g;
|
|
12
|
+
|
|
13
|
+
const DEFAULT_STUN_SERVERS: string[] = ["stun:stun.l.google.com:19302"];
|
|
14
|
+
const DEFAULT_TIMEOUT_MS = 3000;
|
|
15
|
+
|
|
16
|
+
export interface UseWebRTCIPOptions {
|
|
17
|
+
/** STUN server URLs (default: Google STUN). */
|
|
18
|
+
stunServers?: string[];
|
|
19
|
+
/** Stop gathering after this many ms (default: 3000). */
|
|
20
|
+
timeout?: number;
|
|
21
|
+
/** Called once per newly detected IP (no duplicates). */
|
|
22
|
+
onDetect?: (ip: string) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseWebRTCIPReturn {
|
|
26
|
+
/** Unique IPv4 addresses found from ICE candidates. */
|
|
27
|
+
ips: string[];
|
|
28
|
+
/** True while ICE gathering is in progress. */
|
|
29
|
+
loading: boolean;
|
|
30
|
+
/** Error message if WebRTC is unavailable or detection fails. */
|
|
31
|
+
error: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isSSR(): boolean {
|
|
35
|
+
return typeof window === "undefined";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isWebRTCAvailable(): boolean {
|
|
39
|
+
return typeof RTCPeerConnection !== "undefined";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts IPv4 addresses from an ICE candidate string.
|
|
44
|
+
* Filters out common non-public/local patterns (e.g. 0.0.0.0) if desired; currently returns all matches.
|
|
45
|
+
*/
|
|
46
|
+
function extractIPv4FromCandidate(candidate: string): string[] {
|
|
47
|
+
const matches = candidate.match(IPV4_REGEX);
|
|
48
|
+
return matches ? [...matches] : [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Attempts to detect client IP addresses using WebRTC ICE candidates and a STUN server.
|
|
53
|
+
* Works frontend-only (no backend). Not guaranteed to return a public IP; use as a hint and
|
|
54
|
+
* fall back to a public IP API (e.g. ipapi.co, ip-api.com) if you need reliability.
|
|
55
|
+
*
|
|
56
|
+
* @param options - Optional: stunServers, timeout (ms), onDetect(ip) callback.
|
|
57
|
+
* @returns { ips, loading, error } – unique IPv4s, loading flag, and error message.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const { ips, loading, error } = useWebRTCIP({
|
|
61
|
+
* timeout: 5000,
|
|
62
|
+
* onDetect: (ip) => console.log('Detected:', ip),
|
|
63
|
+
* })
|
|
64
|
+
* // If ips is empty and error is set, fall back to: fetch('https://api.ipify.org?format=json')
|
|
65
|
+
*/
|
|
66
|
+
export function useWebRTCIP(
|
|
67
|
+
options: UseWebRTCIPOptions = {}
|
|
68
|
+
): UseWebRTCIPReturn {
|
|
69
|
+
const {
|
|
70
|
+
stunServers = DEFAULT_STUN_SERVERS,
|
|
71
|
+
timeout: timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
72
|
+
onDetect,
|
|
73
|
+
} = options;
|
|
74
|
+
|
|
75
|
+
const [ips, setIps] = useState<string[]>([]);
|
|
76
|
+
const [loading, setLoading] = useState(true);
|
|
77
|
+
const [error, setError] = useState<string | null>(null);
|
|
78
|
+
|
|
79
|
+
const pcRef = useRef<RTCPeerConnection | null>(null);
|
|
80
|
+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
81
|
+
const reportedRef = useRef<Set<string>>(new Set());
|
|
82
|
+
const onDetectRef = useRef(onDetect);
|
|
83
|
+
onDetectRef.current = onDetect;
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (isSSR()) {
|
|
87
|
+
setLoading(false);
|
|
88
|
+
setError("WebRTC IP detection is not available during SSR");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!isWebRTCAvailable()) {
|
|
93
|
+
setLoading(false);
|
|
94
|
+
setError("RTCPeerConnection is not available");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const reported = new Set<string>();
|
|
99
|
+
reportedRef.current = reported;
|
|
100
|
+
|
|
101
|
+
const finish = () => {
|
|
102
|
+
if (timeoutRef.current) {
|
|
103
|
+
clearTimeout(timeoutRef.current);
|
|
104
|
+
timeoutRef.current = null;
|
|
105
|
+
}
|
|
106
|
+
if (pcRef.current) {
|
|
107
|
+
pcRef.current.close();
|
|
108
|
+
pcRef.current = null;
|
|
109
|
+
}
|
|
110
|
+
setLoading(false);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const addIP = (ip: string) => {
|
|
114
|
+
if (reported.has(ip)) return;
|
|
115
|
+
reported.add(ip);
|
|
116
|
+
setIps((prev) => {
|
|
117
|
+
const next = [...prev, ip];
|
|
118
|
+
return next;
|
|
119
|
+
});
|
|
120
|
+
onDetectRef.current?.(ip);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const pc = new RTCPeerConnection({
|
|
125
|
+
iceServers: [{ urls: stunServers }],
|
|
126
|
+
});
|
|
127
|
+
pcRef.current = pc;
|
|
128
|
+
|
|
129
|
+
pc.onicecandidate = (event) => {
|
|
130
|
+
const c = event.candidate;
|
|
131
|
+
if (!c || !c.candidate) return;
|
|
132
|
+
const found = extractIPv4FromCandidate(c.candidate);
|
|
133
|
+
found.forEach(addIP);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
pc.createDataChannel("");
|
|
137
|
+
|
|
138
|
+
pc.createOffer()
|
|
139
|
+
.then((offer) => pc.setLocalDescription(offer))
|
|
140
|
+
.catch((err) => {
|
|
141
|
+
setError(
|
|
142
|
+
err instanceof Error ? err.message : "Failed to create offer"
|
|
143
|
+
);
|
|
144
|
+
finish();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
timeoutRef.current = setTimeout(() => finish(), timeoutMs);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
setError(err instanceof Error ? err.message : "WebRTC setup failed");
|
|
150
|
+
finish();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return () => {
|
|
154
|
+
finish();
|
|
155
|
+
};
|
|
156
|
+
}, [stunServers.join(","), timeoutMs]);
|
|
157
|
+
|
|
158
|
+
return { ips, loading, error };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/*
|
|
162
|
+
* Example usage (Preact component):
|
|
163
|
+
*
|
|
164
|
+
* function MyIPDisplay() {
|
|
165
|
+
* const { ips, loading, error } = useWebRTCIP({
|
|
166
|
+
* timeout: 4000,
|
|
167
|
+
* onDetect: (ip) => { / * optional: e.g. send to analytics * / },
|
|
168
|
+
* })
|
|
169
|
+
*
|
|
170
|
+
* if (loading) return <p>Detecting IP…</p>
|
|
171
|
+
* if (error) return <p>WebRTC failed: {error}. Try fallback API.</p>
|
|
172
|
+
* return <p>IPs (WebRTC): {ips.join(', ') || 'None'}</p>
|
|
173
|
+
* }
|
|
174
|
+
*
|
|
175
|
+
* Fallback to public IP API when WebRTC fails or returns empty:
|
|
176
|
+
* const [apiIP, setApiIP] = useState<string | null>(null)
|
|
177
|
+
* useEffect(() => {
|
|
178
|
+
* if (!loading && ips.length === 0 && error)
|
|
179
|
+
* fetch('https://api.ipify.org?format=json').then(r => r.json()).then(d => setApiIP(d.ip))
|
|
180
|
+
* }, [loading, ips.length, error])
|
|
181
|
+
*/
|
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
* @module useWorkerNotifications
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useState, useRef, useEffect, useMemo } from
|
|
6
|
+
import { useState, useRef, useEffect, useMemo } from "preact/hooks";
|
|
7
7
|
|
|
8
8
|
/** Supported worker event types for tracking. Worker should postMessage with these shapes. */
|
|
9
|
-
export type WorkerEventType =
|
|
9
|
+
export type WorkerEventType =
|
|
10
|
+
| "task_start"
|
|
11
|
+
| "task_end"
|
|
12
|
+
| "task_fail"
|
|
13
|
+
| "queue_size";
|
|
10
14
|
|
|
11
15
|
export interface WorkerNotificationEvent {
|
|
12
16
|
type: WorkerEventType;
|
|
@@ -53,21 +57,21 @@ export interface UseWorkerNotificationsReturn {
|
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
function parseMessage(data: unknown): WorkerNotificationEvent | null {
|
|
56
|
-
if (data == null || typeof data !==
|
|
60
|
+
if (data == null || typeof data !== "object") return null;
|
|
57
61
|
const d = data as Record<string, unknown>;
|
|
58
62
|
const type = d.type as string;
|
|
59
63
|
if (
|
|
60
|
-
type !==
|
|
61
|
-
type !==
|
|
62
|
-
type !==
|
|
63
|
-
type !==
|
|
64
|
+
type !== "task_start" &&
|
|
65
|
+
type !== "task_end" &&
|
|
66
|
+
type !== "task_fail" &&
|
|
67
|
+
type !== "queue_size"
|
|
64
68
|
) {
|
|
65
69
|
return null;
|
|
66
70
|
}
|
|
67
|
-
const taskId = typeof d.taskId ===
|
|
68
|
-
const duration = typeof d.duration ===
|
|
69
|
-
const error = typeof d.error ===
|
|
70
|
-
const size = typeof d.size ===
|
|
71
|
+
const taskId = typeof d.taskId === "string" ? d.taskId : undefined;
|
|
72
|
+
const duration = typeof d.duration === "number" ? d.duration : undefined;
|
|
73
|
+
const error = typeof d.error === "string" ? d.error : undefined;
|
|
74
|
+
const size = typeof d.size === "number" ? d.size : undefined;
|
|
71
75
|
return {
|
|
72
76
|
type: type as WorkerEventType,
|
|
73
77
|
taskId,
|
|
@@ -96,7 +100,9 @@ export function useWorkerNotifications(
|
|
|
96
100
|
const [runningTasks, setRunningTasks] = useState<string[]>([]);
|
|
97
101
|
const [completedCount, setCompletedCount] = useState(0);
|
|
98
102
|
const [failedCount, setFailedCount] = useState(0);
|
|
99
|
-
const [eventHistory, setEventHistory] = useState<WorkerNotificationEvent[]>(
|
|
103
|
+
const [eventHistory, setEventHistory] = useState<WorkerNotificationEvent[]>(
|
|
104
|
+
[]
|
|
105
|
+
);
|
|
100
106
|
const [currentQueueSize, setCurrentQueueSize] = useState(0);
|
|
101
107
|
|
|
102
108
|
const completedTimestampsRef = useRef<number[]>([]);
|
|
@@ -115,11 +121,11 @@ export function useWorkerNotifications(
|
|
|
115
121
|
return next;
|
|
116
122
|
});
|
|
117
123
|
|
|
118
|
-
if (ev.type ===
|
|
124
|
+
if (ev.type === "task_start" && ev.taskId) {
|
|
119
125
|
setRunningTasks((prev) =>
|
|
120
126
|
prev.includes(ev.taskId!) ? prev : [...prev, ev.taskId!]
|
|
121
127
|
);
|
|
122
|
-
} else if (ev.type ===
|
|
128
|
+
} else if (ev.type === "task_end") {
|
|
123
129
|
if (ev.taskId) {
|
|
124
130
|
setRunningTasks((prev) => prev.filter((id) => id !== ev.taskId));
|
|
125
131
|
}
|
|
@@ -129,22 +135,22 @@ export function useWorkerNotifications(
|
|
|
129
135
|
...completedTimestampsRef.current.filter((t) => t >= cutoff),
|
|
130
136
|
ev.timestamp,
|
|
131
137
|
];
|
|
132
|
-
if (typeof ev.duration ===
|
|
138
|
+
if (typeof ev.duration === "number") {
|
|
133
139
|
durationSumRef.current += ev.duration;
|
|
134
140
|
durationCountRef.current += 1;
|
|
135
141
|
}
|
|
136
|
-
} else if (ev.type ===
|
|
142
|
+
} else if (ev.type === "task_fail") {
|
|
137
143
|
if (ev.taskId) {
|
|
138
144
|
setRunningTasks((prev) => prev.filter((id) => id !== ev.taskId));
|
|
139
145
|
}
|
|
140
146
|
setFailedCount((c) => c + 1);
|
|
141
|
-
} else if (ev.type ===
|
|
147
|
+
} else if (ev.type === "queue_size" && typeof ev.size === "number") {
|
|
142
148
|
setCurrentQueueSize(ev.size);
|
|
143
149
|
}
|
|
144
150
|
};
|
|
145
151
|
|
|
146
|
-
worker.addEventListener(
|
|
147
|
-
return () => worker.removeEventListener(
|
|
152
|
+
worker.addEventListener("message", onMessage);
|
|
153
|
+
return () => worker.removeEventListener("message", onMessage);
|
|
148
154
|
}, [worker, maxHistory]);
|
|
149
155
|
|
|
150
156
|
const averageDurationMs = useMemo(() => {
|
|
@@ -156,7 +162,9 @@ export function useWorkerNotifications(
|
|
|
156
162
|
const throughputPerSecond = useMemo(() => {
|
|
157
163
|
const now = Date.now();
|
|
158
164
|
const cutoff = now - throughputWindowMs;
|
|
159
|
-
const timestamps = completedTimestampsRef.current.filter(
|
|
165
|
+
const timestamps = completedTimestampsRef.current.filter(
|
|
166
|
+
(t) => t >= cutoff
|
|
167
|
+
);
|
|
160
168
|
return timestamps.length / (throughputWindowMs / 1000);
|
|
161
169
|
}, [eventHistory, throughputWindowMs]);
|
|
162
170
|
|
|
@@ -1,58 +1,72 @@
|
|
|
1
|
-
import { ComponentChildren, cloneElement, isValidElement } from
|
|
2
|
-
import { useMemo } from
|
|
3
|
-
|
|
4
|
-
export type InjectableProps = Record<string,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
1
|
+
import { ComponentChildren, cloneElement, isValidElement, VNode } from "preact";
|
|
2
|
+
import { useMemo } from "preact/hooks";
|
|
3
|
+
|
|
4
|
+
export type InjectableProps = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
interface PropsWithStyle {
|
|
7
|
+
style?: Record<string, string | number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A Preact hook to wrap children components and inject additional props into them.
|
|
12
|
+
* @param children - The children to wrap and enhance with props.
|
|
13
|
+
* @param injectProps - The props to inject into each child component.
|
|
14
|
+
* @param mergeStrategy - How to handle prop conflicts ('override' | 'preserve'). Defaults to 'preserve'.
|
|
15
|
+
* @returns Enhanced children with injected props.
|
|
16
|
+
*/
|
|
17
|
+
export function useWrappedChildren(
|
|
18
|
+
children: ComponentChildren,
|
|
19
|
+
injectProps: InjectableProps,
|
|
20
|
+
mergeStrategy: "override" | "preserve" = "preserve"
|
|
21
|
+
): ComponentChildren {
|
|
22
|
+
return useMemo(() => {
|
|
23
|
+
if (!children) return children;
|
|
24
|
+
|
|
25
|
+
const enhanceChild = (child: ComponentChildren): ComponentChildren => {
|
|
26
|
+
if (!isValidElement(child)) return child;
|
|
27
|
+
|
|
28
|
+
const existingProps = (child as VNode).props || {};
|
|
29
|
+
|
|
30
|
+
let mergedProps: InjectableProps;
|
|
31
|
+
|
|
32
|
+
if (mergeStrategy === "override") {
|
|
33
|
+
// Injected props override existing ones
|
|
34
|
+
mergedProps = { ...existingProps, ...injectProps };
|
|
35
|
+
} else {
|
|
36
|
+
// Existing props are preserved, injected props are added only if not present
|
|
37
|
+
mergedProps = { ...injectProps, ...existingProps };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Special handling for style prop to merge style objects properly
|
|
41
|
+
const existingStyle = (existingProps as PropsWithStyle)?.style;
|
|
42
|
+
const injectStyle = (injectProps as PropsWithStyle)?.style;
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
existingStyle &&
|
|
46
|
+
injectStyle &&
|
|
47
|
+
typeof existingStyle === "object" &&
|
|
48
|
+
typeof injectStyle === "object"
|
|
49
|
+
) {
|
|
50
|
+
if (mergeStrategy === "override") {
|
|
51
|
+
(mergedProps as PropsWithStyle).style = {
|
|
52
|
+
...existingStyle,
|
|
53
|
+
...injectStyle,
|
|
54
|
+
};
|
|
55
|
+
} else {
|
|
56
|
+
(mergedProps as PropsWithStyle).style = {
|
|
57
|
+
...injectStyle,
|
|
58
|
+
...existingStyle,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return cloneElement(child, mergedProps);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(children)) {
|
|
67
|
+
return children.map(enhanceChild);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return enhanceChild(children);
|
|
71
|
+
}, [children, injectProps, mergeStrategy]);
|
|
72
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter so the same test files can run with React: provides Preact-like exports.
|
|
3
|
+
* Used when running tests with the "react" Vitest project (alias preact → this file).
|
|
4
|
+
*/
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
export const h = React.createElement;
|
|
8
|
+
export const cloneElement = React.cloneElement;
|
|
9
|
+
export const isValidElement = React.isValidElement;
|
|
10
|
+
export type ComponentChildren = React.ReactNode;
|
|
11
|
+
export type RefObject<T> = React.RefObject<T>;
|
|
12
|
+
export type VNode = React.ReactElement;
|