diff --git a/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java b/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java index c45455d..4664b2a 100644 --- a/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java +++ b/src/main/java/com/api/trip/common/sse/emitter/SseEmitterMap.java @@ -15,22 +15,22 @@ @Slf4j public class SseEmitterMap { - private final Map sseEmitterMap = new ConcurrentHashMap<>(); + private final Map sseEmitterMap = new ConcurrentHashMap<>(); - public void put(Long memberId, SseEmitter sseEmitter) { - sseEmitter.onCompletion(() -> remove(memberId)); + public void put(String email, SseEmitter sseEmitter) { + sseEmitter.onCompletion(() -> remove(email)); sseEmitter.onTimeout(sseEmitter::complete); - sseEmitterMap.put(memberId, sseEmitter); - log.info("connected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); + sseEmitterMap.put(email, sseEmitter); + log.info("connected with {}, the number of connections is {}", email, sseEmitterMap.size()); } - public void remove(Long memberId) { - sseEmitterMap.remove(memberId); - log.info("disconnected with {}, the number of connections is {}", memberId, sseEmitterMap.size()); + public void remove(String email) { + sseEmitterMap.remove(email); + log.info("disconnected with {}, the number of connections is {}", email, sseEmitterMap.size()); } - public void send(Long memberId, String eventName, Object eventData) { - SseEmitter sseEmitter = sseEmitterMap.get(memberId); + public void send(String email, String eventName, Object eventData) { + SseEmitter sseEmitter = sseEmitterMap.get(email); try { sseEmitter.send( event() @@ -38,17 +38,17 @@ public void send(Long memberId, String eventName, Object eventData) { .data(eventData) ); } catch (IOException | IllegalStateException e) { - remove(memberId); + remove(email); } } public void sendToAll(String eventName, Object eventData) { SseEventBuilder sseEventBuilder = event().name(eventName).data(eventData); - sseEmitterMap.forEach((memberId, sseEmitter) -> { + sseEmitterMap.forEach((email, sseEmitter) -> { try { sseEmitter.send(sseEventBuilder); } catch (IOException | IllegalStateException e) { - remove(memberId); + remove(email); } }); } diff --git a/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java index 343a1e3..75b151c 100644 --- a/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java +++ b/src/main/java/com/api/trip/domain/notification/controller/NotificationController.java @@ -1,10 +1,9 @@ package com.api.trip.domain.notification.controller; -import com.api.trip.common.exception.CustomException; -import com.api.trip.common.exception.ErrorCode; -import com.api.trip.domain.member.repository.MemberRepository; -import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; import com.api.trip.common.sse.emitter.SseEmitterMap; +import com.api.trip.domain.notification.controller.dto.DeleteNotificationRequest; +import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; +import com.api.trip.domain.notification.controller.dto.ReadNotificationRequest; import com.api.trip.domain.notification.service.NotificationService; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; @@ -20,46 +19,35 @@ @RequiredArgsConstructor public class NotificationController { - private final MemberRepository memberRepository; private final NotificationService notificationService; private final SseEmitterMap sseEmitterMap; @GetMapping(value = "/connect", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public ResponseEntity connect() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); - Long memberId = memberRepository.findByEmail(email) - .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)) - .getId(); - SseEmitter sseEmitter = new SseEmitter(3600000L); - sseEmitterMap.put(memberId, sseEmitter); - sseEmitterMap.send(memberId, "connect", LocalDateTime.now()); + sseEmitterMap.put(email, sseEmitter); + sseEmitterMap.send(email, "connect", LocalDateTime.now()); return ResponseEntity.ok(sseEmitter); } - @GetMapping("/send-to-all") - public void sendToAll(@RequestParam String message) { - String email = SecurityContextHolder.getContext().getAuthentication().getName(); - sseEmitterMap.sendToAll("send-to-all", email + ": " + message); - } - @GetMapping("/me") public ResponseEntity getMyNotifications() { String email = SecurityContextHolder.getContext().getAuthentication().getName(); return ResponseEntity.ok(notificationService.getMyNotifications(email)); } - @PatchMapping("/{notificationId}") - public ResponseEntity readNotification(@PathVariable Long notificationId) { + @PatchMapping + public ResponseEntity readNotification(@RequestBody ReadNotificationRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); - notificationService.readNotification(notificationId, email); + notificationService.readNotification(request, email); return ResponseEntity.ok().build(); } - @DeleteMapping("/{notificationId}") - public ResponseEntity deleteNotification(@PathVariable Long notificationId) { + @DeleteMapping + public ResponseEntity deleteNotification(@RequestBody DeleteNotificationRequest request) { String email = SecurityContextHolder.getContext().getAuthentication().getName(); - notificationService.deleteNotification(notificationId, email); + notificationService.deleteNotification(request, email); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/api/trip/domain/notification/controller/dto/DeleteNotificationRequest.java b/src/main/java/com/api/trip/domain/notification/controller/dto/DeleteNotificationRequest.java new file mode 100644 index 0000000..3e227ba --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/controller/dto/DeleteNotificationRequest.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.notification.controller.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class DeleteNotificationRequest { + + @NotNull(message = "itemId를 입력해 주세요.") + private Long itemId; +} diff --git a/src/main/java/com/api/trip/domain/notification/controller/dto/ReadNotificationRequest.java b/src/main/java/com/api/trip/domain/notification/controller/dto/ReadNotificationRequest.java new file mode 100644 index 0000000..00ec1fb --- /dev/null +++ b/src/main/java/com/api/trip/domain/notification/controller/dto/ReadNotificationRequest.java @@ -0,0 +1,11 @@ +package com.api.trip.domain.notification.controller.dto; + +import jakarta.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class ReadNotificationRequest { + + @NotNull(message = "itemId를 입력해 주세요.") + private Long itemId; +} diff --git a/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java index a9e1430..20f088b 100644 --- a/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java +++ b/src/main/java/com/api/trip/domain/notification/repository/NotificationRepository.java @@ -5,8 +5,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; public interface NotificationRepository extends JpaRepository, NotificationRepositoryCustom { List findAllByMember(Member member); + + Optional findByMemberIdAndItemId(Long memberId, Long itemId); } diff --git a/src/main/java/com/api/trip/domain/notification/service/NotificationService.java b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java index cbaf280..2b66787 100644 --- a/src/main/java/com/api/trip/domain/notification/service/NotificationService.java +++ b/src/main/java/com/api/trip/domain/notification/service/NotificationService.java @@ -7,7 +7,9 @@ import com.api.trip.domain.itemtag.model.ItemTag; import com.api.trip.domain.member.model.Member; import com.api.trip.domain.member.repository.MemberRepository; +import com.api.trip.domain.notification.controller.dto.DeleteNotificationRequest; import com.api.trip.domain.notification.controller.dto.GetMyNotificationsResponse; +import com.api.trip.domain.notification.controller.dto.ReadNotificationRequest; import com.api.trip.domain.notification.domain.Notification; import com.api.trip.domain.notification.repository.NotificationRepository; import lombok.RequiredArgsConstructor; @@ -25,17 +27,17 @@ public class NotificationService { private final NotificationRepository notificationRepository; private final InterestTagService interestTagService; - public void createNotification(Item item, List tagNames){ - + public void createNotification(Item item, List tagNames) { List receivers = interestTagService.getMemberByTags(tagNames); - receivers.stream().forEach(member -> { - notificationRepository.save(Notification.builder() - .item(item) - .member(member).build()); + receivers.forEach(member -> { + notificationRepository.save( + Notification.builder() + .item(item) + .member(member) + .build() + ); }); - - } @Transactional(readOnly = true) @@ -54,31 +56,23 @@ public GetMyNotificationsResponse getMyNotifications(String email) { return GetMyNotificationsResponse.of(notifications, itemTags); } - public void readNotification(Long notificationId, String email) { + public void readNotification(ReadNotificationRequest request, String email) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByMemberIdAndItemId(member.getId(), request.getItemId()) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); - if (notification.getMember() == member) { - throw new CustomException(ErrorCode.FORBIDDEN); - } - notification.read(); } - public void deleteNotification(Long notificationId, String email) { + public void deleteNotification(DeleteNotificationRequest request, String email) { Member member = memberRepository.findByEmail(email) .orElseThrow(() -> new CustomException(ErrorCode.UNAUTHORIZED)); - Notification notification = notificationRepository.findById(notificationId) + Notification notification = notificationRepository.findByMemberIdAndItemId(member.getId(), request.getItemId()) .orElseThrow(() -> new CustomException(ErrorCode.NOTIFICATION_NOT_FOUND)); - if (notification.getMember() == member) { - throw new CustomException(ErrorCode.FORBIDDEN); - } - notificationRepository.delete(notification); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 085b034..5ada501 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,9 +1,6 @@ spring: profiles: active: dev - datasource: - hikari: - maximum-pool-size: 4 h2: console: enabled: true diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html deleted file mode 100644 index af7f606..0000000 --- a/src/main/resources/static/index.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Title - - - - -
- - - - - - diff --git a/src/main/resources/static/src/eventsource.js b/src/main/resources/static/src/eventsource.js deleted file mode 100644 index 980b525..0000000 --- a/src/main/resources/static/src/eventsource.js +++ /dev/null @@ -1,1048 +0,0 @@ -/** @license - * eventsource.js - * Available under MIT License (MIT) - * https://github.com/Yaffle/EventSource/ - */ - -/*jslint indent: 2, vars: true, plusplus: true */ -/*global setTimeout, clearTimeout */ - -(function (global) { - "use strict"; - - var setTimeout = global.setTimeout; - var clearTimeout = global.clearTimeout; - var XMLHttpRequest = global.XMLHttpRequest; - var XDomainRequest = global.XDomainRequest; - var ActiveXObject = global.ActiveXObject; - var NativeEventSource = global.EventSource; - - var document = global.document; - var Promise = global.Promise; - var fetch = global.fetch; - var Response = global.Response; - var TextDecoder = global.TextDecoder; - var TextEncoder = global.TextEncoder; - var AbortController = global.AbortController; - - if (typeof window !== "undefined" && typeof document !== "undefined" && !("readyState" in document) && document.body == null) { // Firefox 2 - document.readyState = "loading"; - window.addEventListener("load", function (event) { - document.readyState = "complete"; - }, false); - } - - if (XMLHttpRequest == null && ActiveXObject != null) { // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest_in_IE6 - XMLHttpRequest = function () { - return new ActiveXObject("Microsoft.XMLHTTP"); - }; - } - - if (Object.create == undefined) { - Object.create = function (C) { - function F(){} - F.prototype = C; - return new F(); - }; - } - - if (!Date.now) { - Date.now = function now() { - return new Date().getTime(); - }; - } - - // see #118 (Promise#finally with polyfilled Promise) - // see #123 (data URLs crash Edge) - // see #125 (CSP violations) - // see pull/#138 - // => No way to polyfill Promise#finally - - if (AbortController == undefined) { - var originalFetch2 = fetch; - fetch = function (url, options) { - var signal = options.signal; - return originalFetch2(url, {headers: options.headers, credentials: options.credentials, cache: options.cache}).then(function (response) { - var reader = response.body.getReader(); - signal._reader = reader; - if (signal._aborted) { - signal._reader.cancel(); - } - return { - status: response.status, - statusText: response.statusText, - headers: response.headers, - body: { - getReader: function () { - return reader; - } - } - }; - }); - }; - AbortController = function () { - this.signal = { - _reader: null, - _aborted: false - }; - this.abort = function () { - if (this.signal._reader != null) { - this.signal._reader.cancel(); - } - this.signal._aborted = true; - }; - }; - } - - function TextDecoderPolyfill() { - this.bitsNeeded = 0; - this.codePoint = 0; - } - - TextDecoderPolyfill.prototype.decode = function (octets) { - function valid(codePoint, shift, octetsCount) { - if (octetsCount === 1) { - return codePoint >= 0x0080 >> shift && codePoint << shift <= 0x07FF; - } - if (octetsCount === 2) { - return codePoint >= 0x0800 >> shift && codePoint << shift <= 0xD7FF || codePoint >= 0xE000 >> shift && codePoint << shift <= 0xFFFF; - } - if (octetsCount === 3) { - return codePoint >= 0x010000 >> shift && codePoint << shift <= 0x10FFFF; - } - throw new Error(); - } - function octetsCount(bitsNeeded, codePoint) { - if (bitsNeeded === 6 * 1) { - return codePoint >> 6 > 15 ? 3 : codePoint > 31 ? 2 : 1; - } - if (bitsNeeded === 6 * 2) { - return codePoint > 15 ? 3 : 2; - } - if (bitsNeeded === 6 * 3) { - return 3; - } - throw new Error(); - } - var REPLACER = 0xFFFD; - var string = ""; - var bitsNeeded = this.bitsNeeded; - var codePoint = this.codePoint; - for (var i = 0; i < octets.length; i += 1) { - var octet = octets[i]; - if (bitsNeeded !== 0) { - if (octet < 128 || octet > 191 || !valid(codePoint << 6 | octet & 63, bitsNeeded - 6, octetsCount(bitsNeeded, codePoint))) { - bitsNeeded = 0; - codePoint = REPLACER; - string += String.fromCharCode(codePoint); - } - } - if (bitsNeeded === 0) { - if (octet >= 0 && octet <= 127) { - bitsNeeded = 0; - codePoint = octet; - } else if (octet >= 192 && octet <= 223) { - bitsNeeded = 6 * 1; - codePoint = octet & 31; - } else if (octet >= 224 && octet <= 239) { - bitsNeeded = 6 * 2; - codePoint = octet & 15; - } else if (octet >= 240 && octet <= 247) { - bitsNeeded = 6 * 3; - codePoint = octet & 7; - } else { - bitsNeeded = 0; - codePoint = REPLACER; - } - if (bitsNeeded !== 0 && !valid(codePoint, bitsNeeded, octetsCount(bitsNeeded, codePoint))) { - bitsNeeded = 0; - codePoint = REPLACER; - } - } else { - bitsNeeded -= 6; - codePoint = codePoint << 6 | octet & 63; - } - if (bitsNeeded === 0) { - if (codePoint <= 0xFFFF) { - string += String.fromCharCode(codePoint); - } else { - string += String.fromCharCode(0xD800 + (codePoint - 0xFFFF - 1 >> 10)); - string += String.fromCharCode(0xDC00 + (codePoint - 0xFFFF - 1 & 0x3FF)); - } - } - } - this.bitsNeeded = bitsNeeded; - this.codePoint = codePoint; - return string; - }; - - // Firefox < 38 throws an error with stream option - var supportsStreamOption = function () { - try { - return new TextDecoder().decode(new TextEncoder().encode("test"), {stream: true}) === "test"; - } catch (error) { - console.debug("TextDecoder does not support streaming option. Using polyfill instead: " + error); - } - return false; - }; - - // IE, Edge - if (TextDecoder == undefined || TextEncoder == undefined || !supportsStreamOption()) { - TextDecoder = TextDecoderPolyfill; - } - - var k = function () { - }; - - function XHRWrapper(xhr) { - this.withCredentials = false; - this.readyState = 0; - this.status = 0; - this.statusText = ""; - this.responseText = ""; - this.onprogress = k; - this.onload = k; - this.onerror = k; - this.onreadystatechange = k; - this._contentType = ""; - this._xhr = xhr; - this._sendTimeout = 0; - this._abort = k; - } - - XHRWrapper.prototype.open = function (method, url) { - this._abort(true); - - var that = this; - var xhr = this._xhr; - var state = 1; - var timeout = 0; - - this._abort = function (silent) { - if (that._sendTimeout !== 0) { - clearTimeout(that._sendTimeout); - that._sendTimeout = 0; - } - if (state === 1 || state === 2 || state === 3) { - state = 4; - xhr.onload = k; - xhr.onerror = k; - xhr.onabort = k; - xhr.onprogress = k; - xhr.onreadystatechange = k; - // IE 8 - 9: XDomainRequest#abort() does not fire any event - // Opera < 10: XMLHttpRequest#abort() does not fire any event - xhr.abort(); - if (timeout !== 0) { - clearTimeout(timeout); - timeout = 0; - } - if (!silent) { - that.readyState = 4; - that.onabort(null); - that.onreadystatechange(); - } - } - state = 0; - }; - - var onStart = function () { - if (state === 1) { - //state = 2; - var status = 0; - var statusText = ""; - var contentType = undefined; - if (!("contentType" in xhr)) { - try { - status = xhr.status; - statusText = xhr.statusText; - contentType = xhr.getResponseHeader("Content-Type"); - } catch (error) { - // IE < 10 throws exception for `xhr.status` when xhr.readyState === 2 || xhr.readyState === 3 - // Opera < 11 throws exception for `xhr.status` when xhr.readyState === 2 - // https://bugs.webkit.org/show_bug.cgi?id=29121 - status = 0; - statusText = ""; - contentType = undefined; - // Firefox < 14, Chrome ?, Safari ? - // https://bugs.webkit.org/show_bug.cgi?id=29658 - // https://bugs.webkit.org/show_bug.cgi?id=77854 - } - } else { - status = 200; - statusText = "OK"; - contentType = xhr.contentType; - } - if (status !== 0) { - state = 2; - that.readyState = 2; - that.status = status; - that.statusText = statusText; - that._contentType = contentType; - that.onreadystatechange(); - } - } - }; - var onProgress = function () { - onStart(); - if (state === 2 || state === 3) { - state = 3; - var responseText = ""; - try { - responseText = xhr.responseText; - } catch (error) { - // IE 8 - 9 with XMLHttpRequest - } - that.readyState = 3; - that.responseText = responseText; - that.onprogress(); - } - }; - var onFinish = function (type, event) { - if (event == null || event.preventDefault == null) { - event = { - preventDefault: k - }; - } - // Firefox 52 fires "readystatechange" (xhr.readyState === 4) without final "readystatechange" (xhr.readyState === 3) - // IE 8 fires "onload" without "onprogress" - onProgress(); - if (state === 1 || state === 2 || state === 3) { - state = 4; - if (timeout !== 0) { - clearTimeout(timeout); - timeout = 0; - } - that.readyState = 4; - if (type === "load") { - that.onload(event); - } else if (type === "error") { - that.onerror(event); - } else if (type === "abort") { - that.onabort(event); - } else { - throw new TypeError(); - } - that.onreadystatechange(); - } - }; - var onReadyStateChange = function (event) { - if (xhr != undefined) { // Opera 12 - if (xhr.readyState === 4) { - if (!("onload" in xhr) || !("onerror" in xhr) || !("onabort" in xhr)) { - onFinish(xhr.responseText === "" ? "error" : "load", event); - } - } else if (xhr.readyState === 3) { - if (!("onprogress" in xhr)) { // testing XMLHttpRequest#responseText too many times is too slow in IE 11 - // and in Firefox 3.6 - onProgress(); - } - } else if (xhr.readyState === 2) { - onStart(); - } - } - }; - var onTimeout = function () { - timeout = setTimeout(function () { - onTimeout(); - }, 500); - if (xhr.readyState === 3) { - onProgress(); - } - }; - - // XDomainRequest#abort removes onprogress, onerror, onload - if ("onload" in xhr) { - xhr.onload = function (event) { - onFinish("load", event); - }; - } - if ("onerror" in xhr) { - xhr.onerror = function (event) { - onFinish("error", event); - }; - } - // improper fix to match Firefox behaviour, but it is better than just ignore abort - // see https://bugzilla.mozilla.org/show_bug.cgi?id=768596 - // https://bugzilla.mozilla.org/show_bug.cgi?id=880200 - // https://code.google.com/p/chromium/issues/detail?id=153570 - // IE 8 fires "onload" without "onprogress - if ("onabort" in xhr) { - xhr.onabort = function (event) { - onFinish("abort", event); - }; - } - - if ("onprogress" in xhr) { - xhr.onprogress = onProgress; - } - - // IE 8 - 9 (XMLHTTPRequest) - // Opera < 12 - // Firefox < 3.5 - // Firefox 3.5 - 3.6 - ? < 9.0 - // onprogress is not fired sometimes or delayed - // see also #64 (significant lag in IE 11) - if ("onreadystatechange" in xhr) { - xhr.onreadystatechange = function (event) { - onReadyStateChange(event); - }; - } - - if ("contentType" in xhr || !("ontimeout" in XMLHttpRequest.prototype)) { - url += (url.indexOf("?") === -1 ? "?" : "&") + "padding=true"; - } - xhr.open(method, url, true); - - if ("readyState" in xhr) { - // workaround for Opera 12 issue with "progress" events - // #91 (XMLHttpRequest onprogress not fired for streaming response in Edge 14-15-?) - timeout = setTimeout(function () { - onTimeout(); - }, 0); - } - }; - XHRWrapper.prototype.abort = function () { - this._abort(false); - }; - XHRWrapper.prototype.getResponseHeader = function (name) { - return this._contentType; - }; - XHRWrapper.prototype.setRequestHeader = function (name, value) { - var xhr = this._xhr; - if ("setRequestHeader" in xhr) { - xhr.setRequestHeader(name, value); - } - }; - XHRWrapper.prototype.getAllResponseHeaders = function () { - // XMLHttpRequest#getAllResponseHeaders returns null for CORS requests in Firefox 3.6.28 - return this._xhr.getAllResponseHeaders != undefined ? this._xhr.getAllResponseHeaders() || "" : ""; - }; - XHRWrapper.prototype.send = function () { - // loading indicator in Safari < ? (6), Chrome < 14, Firefox - // https://bugzilla.mozilla.org/show_bug.cgi?id=736723 - if ((!("ontimeout" in XMLHttpRequest.prototype) || (!("sendAsBinary" in XMLHttpRequest.prototype) && !("mozAnon" in XMLHttpRequest.prototype))) && - document != undefined && - document.readyState != undefined && - document.readyState !== "complete") { - var that = this; - that._sendTimeout = setTimeout(function () { - that._sendTimeout = 0; - that.send(); - }, 4); - return; - } - - var xhr = this._xhr; - // withCredentials should be set after "open" for Safari and Chrome (< 19 ?) - if ("withCredentials" in xhr) { - xhr.withCredentials = this.withCredentials; - } - try { - // xhr.send(); throws "Not enough arguments" in Firefox 3.0 - xhr.send(undefined); - } catch (error1) { - // Safari 5.1.7, Opera 12 - throw error1; - } - }; - - function toLowerCase(name) { - return name.replace(/[A-Z]/g, function (c) { - return String.fromCharCode(c.charCodeAt(0) + 0x20); - }); - } - - function HeadersPolyfill(all) { - // Get headers: implemented according to mozilla's example code: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/getAllResponseHeaders#Example - var map = Object.create(null); - var array = all.split("\r\n"); - for (var i = 0; i < array.length; i += 1) { - var line = array[i]; - var parts = line.split(": "); - var name = parts.shift(); - var value = parts.join(": "); - map[toLowerCase(name)] = value; - } - this._map = map; - } - HeadersPolyfill.prototype.get = function (name) { - return this._map[toLowerCase(name)]; - }; - - if (XMLHttpRequest != null && XMLHttpRequest.HEADERS_RECEIVED == null) { // IE < 9, Firefox 3.6 - XMLHttpRequest.HEADERS_RECEIVED = 2; - } - - function XHRTransport() { - } - - XHRTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { - xhr.open("GET", url); - var offset = 0; - xhr.onprogress = function () { - var responseText = xhr.responseText; - var chunk = responseText.slice(offset); - offset += chunk.length; - onProgressCallback(chunk); - }; - xhr.onerror = function (event) { - event.preventDefault(); - onFinishCallback(new Error("NetworkError")); - }; - xhr.onload = function () { - onFinishCallback(null); - }; - xhr.onabort = function () { - onFinishCallback(null); - }; - xhr.onreadystatechange = function () { - if (xhr.readyState === XMLHttpRequest.HEADERS_RECEIVED) { - var status = xhr.status; - var statusText = xhr.statusText; - var contentType = xhr.getResponseHeader("Content-Type"); - var headers = xhr.getAllResponseHeaders(); - onStartCallback(status, statusText, contentType, new HeadersPolyfill(headers)); - } - }; - xhr.withCredentials = withCredentials; - for (var name in headers) { - if (Object.prototype.hasOwnProperty.call(headers, name)) { - xhr.setRequestHeader(name, headers[name]); - } - } - xhr.send(); - return xhr; - }; - - function HeadersWrapper(headers) { - this._headers = headers; - } - HeadersWrapper.prototype.get = function (name) { - return this._headers.get(name); - }; - - function FetchTransport() { - } - - FetchTransport.prototype.open = function (xhr, onStartCallback, onProgressCallback, onFinishCallback, url, withCredentials, headers) { - var reader = null; - var controller = new AbortController(); - var signal = controller.signal; - var textDecoder = new TextDecoder(); - fetch(url, { - headers: headers, - credentials: withCredentials ? "include" : "same-origin", - signal: signal, - cache: "no-store" - }).then(function (response) { - reader = response.body.getReader(); - onStartCallback(response.status, response.statusText, response.headers.get("Content-Type"), new HeadersWrapper(response.headers)); - // see https://github.com/promises-aplus/promises-spec/issues/179 - return new Promise(function (resolve, reject) { - var readNextChunk = function () { - reader.read().then(function (result) { - if (result.done) { - //Note: bytes in textDecoder are ignored - resolve(undefined); - } else { - var chunk = textDecoder.decode(result.value, {stream: true}); - onProgressCallback(chunk); - readNextChunk(); - } - })["catch"](function (error) { - reject(error); - }); - }; - readNextChunk(); - }); - })["catch"](function (error) { - if (error.name === "AbortError") { - return undefined; - } else { - return error; - } - }).then(function (error) { - onFinishCallback(error); - }); - return { - abort: function () { - if (reader != null) { - reader.cancel(); // https://bugzilla.mozilla.org/show_bug.cgi?id=1583815 - } - controller.abort(); - } - }; - }; - - function EventTarget() { - this._listeners = Object.create(null); - } - - function throwError(e) { - setTimeout(function () { - throw e; - }, 0); - } - - EventTarget.prototype.dispatchEvent = function (event) { - event.target = this; - var typeListeners = this._listeners[event.type]; - if (typeListeners != undefined) { - var length = typeListeners.length; - for (var i = 0; i < length; i += 1) { - var listener = typeListeners[i]; - try { - if (typeof listener.handleEvent === "function") { - listener.handleEvent(event); - } else { - listener.call(this, event); - } - } catch (e) { - throwError(e); - } - } - } - }; - EventTarget.prototype.addEventListener = function (type, listener) { - type = String(type); - var listeners = this._listeners; - var typeListeners = listeners[type]; - if (typeListeners == undefined) { - typeListeners = []; - listeners[type] = typeListeners; - } - var found = false; - for (var i = 0; i < typeListeners.length; i += 1) { - if (typeListeners[i] === listener) { - found = true; - } - } - if (!found) { - typeListeners.push(listener); - } - }; - EventTarget.prototype.removeEventListener = function (type, listener) { - type = String(type); - var listeners = this._listeners; - var typeListeners = listeners[type]; - if (typeListeners != undefined) { - var filtered = []; - for (var i = 0; i < typeListeners.length; i += 1) { - if (typeListeners[i] !== listener) { - filtered.push(typeListeners[i]); - } - } - if (filtered.length === 0) { - delete listeners[type]; - } else { - listeners[type] = filtered; - } - } - }; - - function Event(type) { - this.type = type; - this.target = undefined; - } - - function MessageEvent(type, options) { - Event.call(this, type); - this.data = options.data; - this.lastEventId = options.lastEventId; - } - - MessageEvent.prototype = Object.create(Event.prototype); - - function ConnectionEvent(type, options) { - Event.call(this, type); - this.status = options.status; - this.statusText = options.statusText; - this.headers = options.headers; - } - - ConnectionEvent.prototype = Object.create(Event.prototype); - - function ErrorEvent(type, options) { - Event.call(this, type); - this.error = options.error; - } - - ErrorEvent.prototype = Object.create(Event.prototype); - - var WAITING = -1; - var CONNECTING = 0; - var OPEN = 1; - var CLOSED = 2; - - var AFTER_CR = -1; - var FIELD_START = 0; - var FIELD = 1; - var VALUE_START = 2; - var VALUE = 3; - - var contentTypeRegExp = /^text\/event\-stream(;.*)?$/i; - - var MINIMUM_DURATION = 1000; - var MAXIMUM_DURATION = 18000000; - - var parseDuration = function (value, def) { - var n = value == null ? def : parseInt(value, 10); - if (n !== n) { - n = def; - } - return clampDuration(n); - }; - var clampDuration = function (n) { - return Math.min(Math.max(n, MINIMUM_DURATION), MAXIMUM_DURATION); - }; - - var fire = function (that, f, event) { - try { - if (typeof f === "function") { - f.call(that, event); - } - } catch (e) { - throwError(e); - } - }; - - function EventSourcePolyfill(url, options) { - EventTarget.call(this); - options = options || {}; - - this.onopen = undefined; - this.onmessage = undefined; - this.onerror = undefined; - - this.url = undefined; - this.readyState = undefined; - this.withCredentials = undefined; - this.headers = undefined; - - this._close = undefined; - - start(this, url, options); - } - - function getBestXHRTransport() { - return (XMLHttpRequest != undefined && ("withCredentials" in XMLHttpRequest.prototype)) || XDomainRequest == undefined - ? new XMLHttpRequest() - : new XDomainRequest(); - } - - var isFetchSupported = fetch != undefined && Response != undefined && "body" in Response.prototype; - - function start(es, url, options) { - url = String(url); - var withCredentials = Boolean(options.withCredentials); - var lastEventIdQueryParameterName = options.lastEventIdQueryParameterName || "lastEventId"; - - var initialRetry = clampDuration(1000); - var heartbeatTimeout = parseDuration(options.heartbeatTimeout, 45000); - - var lastEventId = ""; - var retry = initialRetry; - var wasActivity = false; - var textLength = 0; - var headers = options.headers || {}; - var TransportOption = options.Transport; - var xhr = isFetchSupported && TransportOption == undefined ? undefined : new XHRWrapper(TransportOption != undefined ? new TransportOption() : getBestXHRTransport()); - var transport = TransportOption != null && typeof TransportOption !== "string" ? new TransportOption() : (xhr == undefined ? new FetchTransport() : new XHRTransport()); - var abortController = undefined; - var timeout = 0; - var currentState = WAITING; - var dataBuffer = ""; - var lastEventIdBuffer = ""; - var eventTypeBuffer = ""; - - var textBuffer = ""; - var state = FIELD_START; - var fieldStart = 0; - var valueStart = 0; - - var onStart = function (status, statusText, contentType, headers) { - if (currentState === CONNECTING) { - if (status === 200 && contentType != undefined && contentTypeRegExp.test(contentType)) { - currentState = OPEN; - wasActivity = Date.now(); - retry = initialRetry; - es.readyState = OPEN; - var event = new ConnectionEvent("open", { - status: status, - statusText: statusText, - headers: headers - }); - es.dispatchEvent(event); - fire(es, es.onopen, event); - } else { - var message = ""; - if (status !== 200) { - if (statusText) { - statusText = statusText.replace(/\s+/g, " "); - } - message = "EventSource's response has a status " + status + " " + statusText + " that is not 200. Aborting the connection."; - } else { - message = "EventSource's response has a Content-Type specifying an unsupported type: " + (contentType == undefined ? "-" : contentType.replace(/\s+/g, " ")) + ". Aborting the connection."; - } - close(); - var event = new ConnectionEvent("error", { - status: status, - statusText: statusText, - headers: headers - }); - es.dispatchEvent(event); - fire(es, es.onerror, event); - console.error(message); - } - } - }; - - var onProgress = function (textChunk) { - if (currentState === OPEN) { - var n = -1; - for (var i = 0; i < textChunk.length; i += 1) { - var c = textChunk.charCodeAt(i); - if (c === "\n".charCodeAt(0) || c === "\r".charCodeAt(0)) { - n = i; - } - } - var chunk = (n !== -1 ? textBuffer : "") + textChunk.slice(0, n + 1); - textBuffer = (n === -1 ? textBuffer : "") + textChunk.slice(n + 1); - if (textChunk !== "") { - wasActivity = Date.now(); - textLength += textChunk.length; - } - for (var position = 0; position < chunk.length; position += 1) { - var c = chunk.charCodeAt(position); - if (state === AFTER_CR && c === "\n".charCodeAt(0)) { - state = FIELD_START; - } else { - if (state === AFTER_CR) { - state = FIELD_START; - } - if (c === "\r".charCodeAt(0) || c === "\n".charCodeAt(0)) { - if (state !== FIELD_START) { - if (state === FIELD) { - valueStart = position + 1; - } - var field = chunk.slice(fieldStart, valueStart - 1); - var value = chunk.slice(valueStart + (valueStart < position && chunk.charCodeAt(valueStart) === " ".charCodeAt(0) ? 1 : 0), position); - if (field === "data") { - dataBuffer += "\n"; - dataBuffer += value; - } else if (field === "id") { - lastEventIdBuffer = value; - } else if (field === "event") { - eventTypeBuffer = value; - } else if (field === "retry") { - initialRetry = parseDuration(value, initialRetry); - retry = initialRetry; - } else if (field === "heartbeatTimeout") { - heartbeatTimeout = parseDuration(value, heartbeatTimeout); - if (timeout !== 0) { - clearTimeout(timeout); - timeout = setTimeout(function () { - onTimeout(); - }, heartbeatTimeout); - } - } - } - if (state === FIELD_START) { - if (dataBuffer !== "") { - lastEventId = lastEventIdBuffer; - if (eventTypeBuffer === "") { - eventTypeBuffer = "message"; - } - var event = new MessageEvent(eventTypeBuffer, { - data: dataBuffer.slice(1), - lastEventId: lastEventIdBuffer - }); - es.dispatchEvent(event); - if (eventTypeBuffer === "open") { - fire(es, es.onopen, event); - } else if (eventTypeBuffer === "message") { - fire(es, es.onmessage, event); - } else if (eventTypeBuffer === "error") { - fire(es, es.onerror, event); - } - if (currentState === CLOSED) { - return; - } - } - dataBuffer = ""; - eventTypeBuffer = ""; - } - state = c === "\r".charCodeAt(0) ? AFTER_CR : FIELD_START; - } else { - if (state === FIELD_START) { - fieldStart = position; - state = FIELD; - } - if (state === FIELD) { - if (c === ":".charCodeAt(0)) { - valueStart = position + 1; - state = VALUE_START; - } - } else if (state === VALUE_START) { - state = VALUE; - } - } - } - } - } - }; - - var onFinish = function (error) { - if (currentState === OPEN || currentState === CONNECTING) { - currentState = WAITING; - if (timeout !== 0) { - clearTimeout(timeout); - timeout = 0; - } - timeout = setTimeout(function () { - onTimeout(); - }, retry); - retry = clampDuration(Math.min(initialRetry * 16, retry * 2)); - - es.readyState = CONNECTING; - var event = new ErrorEvent("error", {error: error}); - es.dispatchEvent(event); - fire(es, es.onerror, event); - if (error != undefined) { - console.error(error); - } - } - }; - - var close = function () { - currentState = CLOSED; - if (abortController != undefined) { - abortController.abort(); - abortController = undefined; - } - if (timeout !== 0) { - clearTimeout(timeout); - timeout = 0; - } - es.readyState = CLOSED; - }; - - var onTimeout = function () { - timeout = 0; - - if (currentState !== WAITING) { - if (!wasActivity && abortController != undefined) { - onFinish(new Error("No activity within " + heartbeatTimeout + " milliseconds." + " " + (currentState === CONNECTING ? "No response received." : textLength + " chars received.") + " " + "Reconnecting.")); - if (abortController != undefined) { - abortController.abort(); - abortController = undefined; - } - } else { - var nextHeartbeat = Math.max((wasActivity || Date.now()) + heartbeatTimeout - Date.now(), 1); - wasActivity = false; - timeout = setTimeout(function () { - onTimeout(); - }, nextHeartbeat); - } - return; - } - - wasActivity = false; - textLength = 0; - timeout = setTimeout(function () { - onTimeout(); - }, heartbeatTimeout); - - currentState = CONNECTING; - dataBuffer = ""; - eventTypeBuffer = ""; - lastEventIdBuffer = lastEventId; - textBuffer = ""; - fieldStart = 0; - valueStart = 0; - state = FIELD_START; - - // https://bugzilla.mozilla.org/show_bug.cgi?id=428916 - // Request header field Last-Event-ID is not allowed by Access-Control-Allow-Headers. - var requestURL = url; - if (url.slice(0, 5) !== "data:" && url.slice(0, 5) !== "blob:") { - if (lastEventId !== "") { - // Remove the lastEventId parameter if it's already part of the request URL. - var i = url.indexOf("?"); - requestURL = i === -1 ? url : url.slice(0, i + 1) + url.slice(i + 1).replace(/(?:^|&)([^=&]*)(?:=[^&]*)?/g, function (p, paramName) { - return paramName === lastEventIdQueryParameterName ? '' : p; - }); - // Append the current lastEventId to the request URL. - requestURL += (url.indexOf("?") === -1 ? "?" : "&") + lastEventIdQueryParameterName +"=" + encodeURIComponent(lastEventId); - } - } - var withCredentials = es.withCredentials; - var requestHeaders = {}; - requestHeaders["Accept"] = "text/event-stream"; - var headers = es.headers; - if (headers != undefined) { - for (var name in headers) { - if (Object.prototype.hasOwnProperty.call(headers, name)) { - requestHeaders[name] = headers[name]; - } - } - } - try { - abortController = transport.open(xhr, onStart, onProgress, onFinish, requestURL, withCredentials, requestHeaders); - } catch (error) { - close(); - throw error; - } - }; - - es.url = url; - es.readyState = CONNECTING; - es.withCredentials = withCredentials; - es.headers = headers; - es._close = close; - - onTimeout(); - } - - EventSourcePolyfill.prototype = Object.create(EventTarget.prototype); - EventSourcePolyfill.prototype.CONNECTING = CONNECTING; - EventSourcePolyfill.prototype.OPEN = OPEN; - EventSourcePolyfill.prototype.CLOSED = CLOSED; - EventSourcePolyfill.prototype.close = function () { - this._close(); - }; - - EventSourcePolyfill.CONNECTING = CONNECTING; - EventSourcePolyfill.OPEN = OPEN; - EventSourcePolyfill.CLOSED = CLOSED; - EventSourcePolyfill.prototype.withCredentials = undefined; - - var R = NativeEventSource - if (XMLHttpRequest != undefined && (NativeEventSource == undefined || !("withCredentials" in NativeEventSource.prototype))) { - // Why replace a native EventSource ? - // https://bugzilla.mozilla.org/show_bug.cgi?id=444328 - // https://bugzilla.mozilla.org/show_bug.cgi?id=831392 - // https://code.google.com/p/chromium/issues/detail?id=260144 - // https://code.google.com/p/chromium/issues/detail?id=225654 - // ... - R = EventSourcePolyfill; - } - - (function (factory) { - if (typeof module === "object" && typeof module.exports === "object") { - var v = factory(exports); - if (v !== undefined) module.exports = v; - } - else if (typeof define === "function" && define.amd) { - define(["exports"], factory); - } - else { - factory(global); - } - })(function (exports) { - exports.EventSourcePolyfill = EventSourcePolyfill; - exports.NativeEventSource = NativeEventSource; - exports.EventSource = R; - }); -}(typeof globalThis === 'undefined' ? (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : this) : globalThis));