#24 simple sns: alarm with sse

This commit is contained in:
haerong22
2022-12-03 00:03:47 +09:00
parent b479560d3e
commit a9be1a58cd
7 changed files with 159 additions and 11 deletions

View File

@@ -47,6 +47,7 @@ import Slide from '@mui/material/Slide';
// Data // Data
import axios from 'axios'; import axios from 'axios';
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement<any, any>, children: React.ReactElement<any, any>,
@@ -61,8 +62,10 @@ function Alarm() {
const [render, setRender] = useState(false); const [render, setRender] = useState(false);
const [alarms, setAlarms] = useState([]); const [alarms, setAlarms] = useState([]);
const [totalPage, setTotalPage] = useState(0); const [totalPage, setTotalPage] = useState(0);
const [alarmEvent, setAlarmEvent] = useState(undefined);
const navigate = useNavigate(); const navigate = useNavigate();
let eventSource = undefined;
const changePage = (pageNum) => { const changePage = (pageNum) => {
console.log('change pages'); console.log('change pages');
@@ -75,7 +78,7 @@ function Alarm() {
const handleGetAlarm = (pageNum, event) => { const handleGetAlarm = (pageNum, event) => {
console.log('handleGetAlarm'); console.log('handleGetAlarm');
axios({ axios({
url: '/api/v1/users/alarm?size=5&sort=id&page=' + pageNum, url: '/api/v1/users/alarm?size=5&sort=id,desc&page=' + pageNum,
method: 'GET', method: 'GET',
headers: { headers: {
Authorization: 'Bearer ' + localStorage.getItem('token'), Authorization: 'Bearer ' + localStorage.getItem('token'),
@@ -95,6 +98,28 @@ function Alarm() {
useEffect(() => { useEffect(() => {
handleGetAlarm(); handleGetAlarm();
eventSource = new EventSource("http://localhost:8080/api/v1/users/alarm/subscribe?token=" + localStorage.getItem('token'));
setAlarmEvent(eventSource);
eventSource.addEventListener("open", function (event) {
console.log("connection opened");
});
eventSource.addEventListener("alarm", function (event) {
console.log(event.data);
handleGetAlarm();
});
eventSource.addEventListener("error", function (event) {
console.log(event.target.readyState);
if (event.target.readyState === EventSource.CLOSED) {
console.log("eventsource closed (" + event.target.readyState + ")");
}
eventSource.close();
});
}, []); }, []);
return ( return (

View File

@@ -16,6 +16,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
import java.util.List;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -23,10 +24,20 @@ public class JwtTokenFilter extends OncePerRequestFilter {
private final String key; private final String key;
private final UserService userService; private final UserService userService;
private final static List<String> TOKEN_IN_PARAM_URLS = List.of(
"/api/v1/users/alarm/subscribe"
);
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String token;
try {
if (TOKEN_IN_PARAM_URLS.contains(request.getRequestURI())) {
log.info("Request with {} check the query param", request.getRequestURI());
token = request.getQueryString().split("=")[1].trim();
} else {
final String header = request.getHeader(HttpHeaders.AUTHORIZATION); final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header == null || !header.startsWith("Bearer ")) { if (header == null || !header.startsWith("Bearer ")) {
@@ -34,9 +45,8 @@ public class JwtTokenFilter extends OncePerRequestFilter {
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
return; return;
} }
token = header.split(" ")[1].trim();
try { }
final String token = header.split(" ")[1].trim();
if (JwtTokenUtils.isExpired(token, key)) { if (JwtTokenUtils.isExpired(token, key)) {
log.error("Token is expired"); log.error("Token is expired");

View File

@@ -9,6 +9,7 @@ import com.example.sns.controller.response.UserLoginResponse;
import com.example.sns.exception.ErrorCode; import com.example.sns.exception.ErrorCode;
import com.example.sns.exception.SnsApplicationException; import com.example.sns.exception.SnsApplicationException;
import com.example.sns.model.User; import com.example.sns.model.User;
import com.example.sns.service.AlarmService;
import com.example.sns.service.UserService; import com.example.sns.service.UserService;
import com.example.sns.util.ClassUtils; import com.example.sns.util.ClassUtils;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -16,6 +17,7 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@RestController @RestController
@RequestMapping("/api/v1/users") @RequestMapping("/api/v1/users")
@@ -23,6 +25,7 @@ import org.springframework.web.bind.annotation.*;
public class UserController { public class UserController {
private final UserService userService; private final UserService userService;
private final AlarmService alarmService;
@PostMapping("/join") @PostMapping("/join")
public Response<UserJoinResponse> join(@RequestBody UserJoinRequest request) { public Response<UserJoinResponse> join(@RequestBody UserJoinRequest request) {
@@ -48,4 +51,17 @@ public class UserController {
); );
return Response.success(userService.alarmList(user.getId(), pageable).map(AlarmResponse::fromAlarm)); return Response.success(userService.alarmList(user.getId(), pageable).map(AlarmResponse::fromAlarm));
} }
@GetMapping("/alarm/subscribe")
public SseEmitter subscribe(Authentication authentication) {
User user = ClassUtils.getSafeCastInstance(authentication.getPrincipal(), User.class)
.orElseThrow(
() -> new SnsApplicationException(
ErrorCode.INTERNAL_SERVER_ERROR,
"Casting to User class failed"
)
);
return alarmService.connectAlarm(user.getId());
}
} }

View File

@@ -16,6 +16,7 @@ public enum ErrorCode {
INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "Permission is invalid."), INVALID_PERMISSION(HttpStatus.UNAUTHORIZED, "Permission is invalid."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error."), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal server error."),
ALREADY_LIKED(HttpStatus.CONFLICT, "User already liked the post."), ALREADY_LIKED(HttpStatus.CONFLICT, "User already liked the post."),
ALARM_CONNECT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Connecting alarm occurs error."),
; ;

View File

@@ -0,0 +1,38 @@
package com.example.sns.repository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Repository
public class EmitterRepository {
private final Map<String, SseEmitter> emitterMap = new HashMap<>();
public SseEmitter save(Integer userId, SseEmitter sseEmitter) {
final String key = getKey(userId);
emitterMap.put(key, sseEmitter);
log.info("Set sseEmitter for {}", userId);
return sseEmitter;
}
public Optional<SseEmitter> get(Integer userId) {
final String key = getKey(userId);
SseEmitter sseEmitter = emitterMap.get(key);
log.info("Get sseEmitter for {}", userId);
return Optional.ofNullable(sseEmitter);
}
public void delete(Integer userid) {
emitterMap.remove(getKey(userid));
}
private String getKey(Integer userId) {
return "Emitter:UID:" + userId;
}
}

View File

@@ -0,0 +1,53 @@
package com.example.sns.service;
import com.example.sns.exception.ErrorCode;
import com.example.sns.exception.SnsApplicationException;
import com.example.sns.repository.EmitterRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
@Slf4j
@Service
@RequiredArgsConstructor
public class AlarmService {
private final static Long DEFAULT_TIMEOUT = 60L * 1000 * 60;
private final static String ALARM_NAME = "alarm";
private final EmitterRepository emitterRepository;
public SseEmitter connectAlarm(Integer userId) {
SseEmitter sseEmitter = new SseEmitter(DEFAULT_TIMEOUT);
emitterRepository.save(userId, sseEmitter);
sseEmitter.onCompletion(() -> {
emitterRepository.delete(userId);
});
sseEmitter.onTimeout(() -> {
emitterRepository.delete(userId);
});
try {
sseEmitter.send(SseEmitter.event().id("").name(ALARM_NAME).data("connect completed"));
} catch (IOException e) {
throw new SnsApplicationException(ErrorCode.ALARM_CONNECT_ERROR);
}
return sseEmitter;
}
public void send(Integer alarmId, Integer userId) {
emitterRepository.get(userId).ifPresentOrElse(sseEmitter -> {
try {
sseEmitter.send(SseEmitter.event().id(alarmId.toString()).name(ALARM_NAME).data("new alarm"));
} catch (IOException e) {
emitterRepository.delete(userId);
throw new SnsApplicationException(ErrorCode.ALARM_CONNECT_ERROR);
}
}, () -> log.info("No emitter founded."));
}
}

View File

@@ -24,6 +24,7 @@ public class PostService {
private final LikeEntityRepository likeEntityRepository; private final LikeEntityRepository likeEntityRepository;
private final CommentEntityRepository commentEntityRepository; private final CommentEntityRepository commentEntityRepository;
private final AlarmEntityRepository alarmEntityRepository; private final AlarmEntityRepository alarmEntityRepository;
private final AlarmService alarmService;
@Transactional @Transactional
public void create(String title, String body, String username) { public void create(String title, String body, String username) {
@@ -94,12 +95,14 @@ public class PostService {
// save like // save like
likeEntityRepository.save(LikeEntity.of(userEntity, postEntity)); likeEntityRepository.save(LikeEntity.of(userEntity, postEntity));
alarmEntityRepository.save(AlarmEntity.of( AlarmEntity alarmEntity = alarmEntityRepository.save(AlarmEntity.of(
postEntity.getUser(), postEntity.getUser(),
AlarmType.NEW_LIKE_ON_POST, AlarmType.NEW_LIKE_ON_POST,
new AlarmArgs(userEntity.getId(), postEntity.getId()) new AlarmArgs(userEntity.getId(), postEntity.getId())
) )
); );
alarmService.send(alarmEntity.getId(), postEntity.getUser().getId());
} }
public long likeCount(Integer postId) { public long likeCount(Integer postId) {
@@ -116,12 +119,14 @@ public class PostService {
commentEntityRepository.save(CommentEntity.of(userEntity, postEntity, comment)); commentEntityRepository.save(CommentEntity.of(userEntity, postEntity, comment));
alarmEntityRepository.save(AlarmEntity.of( AlarmEntity alarmEntity = alarmEntityRepository.save(AlarmEntity.of(
postEntity.getUser(), postEntity.getUser(),
AlarmType.NEW_COMMENT_ON_POST, AlarmType.NEW_COMMENT_ON_POST,
new AlarmArgs(userEntity.getId(), postEntity.getId()) new AlarmArgs(userEntity.getId(), postEntity.getId())
) )
); );
alarmService.send(alarmEntity.getId(), postEntity.getUser().getId());
} }
public Page<Comment> getComments(Integer postId, Pageable pageable) { public Page<Comment> getComments(Integer postId, Pageable pageable) {