Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,33 @@
.theme-toggle:hover {
transform: scale(1.1);
}

.pause-toggle {
position: absolute;
top: 20px;
right: 60px;
z-index: 1000;
background: transparent;
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
padding: 0;
color: var(--text-color);
}

.pause-toggle svg {
display: block;
}

.pause-toggle:hover {
transform: scale(1.1);
}

.icon {
width: 25px;
Expand Down
44 changes: 44 additions & 0 deletions index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,48 @@ const ThemeToggle = () => {
)
}

/**
* Pause toggle button component
* @returns {React.Element} Pause toggle button
*/
const PauseToggle = () => {
const [paused, setPaused] = React.useState(speedtest.isPaused())

React.useEffect(() => {
// Listen for pause state changes
const unsubscribe = speedtest.onPauseChange(setPaused)
return unsubscribe // Cleanup on unmount
}, [])

const handleToggle = () => {
if (paused) {
speedtest.resume()
} else {
speedtest.pause()
}
}

return (
<button
className="pause-toggle"
onClick={handleToggle}
title={paused ? 'Resume testing' : 'Pause testing'}
aria-label={paused ? 'Resume testing' : 'Pause testing'}
>
{paused ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" opacity="0.6">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1" opacity="0.6">
<rect x="6" y="4" width="4" height="16"/>
<rect x="14" y="4" width="4" height="16"/>
</svg>
)}
</button>
)
}

/**
* Progress indicator component for warm-up phase
* @param {Object} props - Component props
Expand All @@ -169,6 +211,7 @@ const ProgressIndicator = ({ progress }) => {
return (
<div>
<ThemeToggle />
<PauseToggle />
<div className="text-center mt-5">
<div className="mb-4">
<div className="spinner-border text-primary mb-3" role="status">
Expand Down Expand Up @@ -339,6 +382,7 @@ const Table = ({ history = [], blockList = [] }) => {
return (
<div>
<ThemeToggle />
<PauseToggle />
<div className="mb-3">
<small className="text-muted">
Testing {history.length + blockList.length} Azure regions | {' '}
Expand Down
2 changes: 1 addition & 1 deletion index.min.js

Large diffs are not rendered by default.

69 changes: 67 additions & 2 deletions lib/speed-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ const state = {
total: 0,
completed: 0,
isWarmupPhase: true
}
},
isPaused: false,
pauseCallbacks: []
}

// Extract destructured state for cleaner code
const { blockList, queue, callbacks, callRecords, errorCallbacks, firstResultDiscarded, progressCallbacks } = state
const { blockList, queue, callbacks, callRecords, errorCallbacks, firstResultDiscarded, progressCallbacks, pauseCallbacks } = state

// Generate a test URL for a location
function getTestUrl(item) {
Expand Down Expand Up @@ -96,6 +98,11 @@ async function testLatency(url) {
* Process the next latency test in the queue
*/
function process() {
// Check if paused - if so, exit (resume will restart)
if (state.isPaused) {
return
}

state.counter += 1

// Refill queue if empty
Expand Down Expand Up @@ -380,3 +387,61 @@ export const getStats = () => ({
queueLength: queue.length,
activeRequests: Object.keys(callRecords).length
})

/**
* Pause all speed test requests
*/
export const pause = () => {
state.isPaused = true
pauseCallbacks.forEach(callback => {
try {
callback(true)
} catch (err) {
console.error('Pause callback error:', err)
}
})
}

/**
* Resume all speed test requests
*/
export const resume = () => {
state.isPaused = false
pauseCallbacks.forEach(callback => {
try {
callback(false)
} catch (err) {
console.error('Pause callback error:', err)
}
})

// Restart workers
for (let i = 0; i < CONFIG.CONCURRENCY; i++) {
setTimeout(process, i * 10)
}
}

/**
* Get current pause state
*/
export const isPaused = () => state.isPaused

/**
* Register a callback for pause state changes
* @param {Function} callback - Called with boolean (true = paused, false = resumed)
* @returns {Function} Unsubscribe function to remove the callback
*/
export const onPauseChange = (callback) => {
if (typeof callback !== 'function') {
throw new Error('Callback must be a function')
}
pauseCallbacks.push(callback)

// Return unsubscribe function
return () => {
const index = pauseCallbacks.indexOf(callback)
if (index > -1) {
pauseCallbacks.splice(index, 1)
}
}
}