From dd3b02bbffb25a3cc8831305702e194587ffba60 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 11 Dec 2019 10:11:40 +0100 Subject: [PATCH] added subscription api to apiClient --- .../{SessionID.java => SessionId.java} | 0 scm-ui/ui-components/package.json | 1 + scm-ui/ui-components/src/apiclient.ts | 99 +++++++++++++++---- yarn.lock | 5 + 4 files changed, 87 insertions(+), 18 deletions(-) rename scm-core/src/main/java/sonia/scm/security/{SessionID.java => SessionId.java} (100%) diff --git a/scm-core/src/main/java/sonia/scm/security/SessionID.java b/scm-core/src/main/java/sonia/scm/security/SessionId.java similarity index 100% rename from scm-core/src/main/java/sonia/scm/security/SessionID.java rename to scm-core/src/main/java/sonia/scm/security/SessionId.java diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 946416ca96..93a08a75f2 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -49,6 +49,7 @@ "@scm-manager/ui-types": "^2.0.0-SNAPSHOT", "classnames": "^2.2.6", "date-fns": "^2.4.1", + "event-source-polyfill": "^1.0.9", "query-string": "5", "react": "^16.8.6", "react-diff-view": "^1.8.1", diff --git a/scm-ui/ui-components/src/apiclient.ts b/scm-ui/ui-components/src/apiclient.ts index bd7f4eb6ab..da2287f72b 100644 --- a/scm-ui/ui-components/src/apiclient.ts +++ b/scm-ui/ui-components/src/apiclient.ts @@ -1,6 +1,36 @@ import { contextPath } from "./urls"; -import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors"; -import { BackendErrorContent } from "./errors"; +// @ts-ignore we have not types for event-source-polyfill +import { EventSourcePolyfill } from "event-source-polyfill"; +import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError, BackendErrorContent } from "./errors"; + +type SubscriptionEvent = { + type: string; +}; + +type OpenEvent = SubscriptionEvent; + +type ErrorEvent = SubscriptionEvent & { + error: Error; +}; + +type MessageEvent = SubscriptionEvent & { + data: string; + lastEventId?: string; +}; + +type MessageListeners = { + [eventType: string]: (event: MessageEvent) => void; +}; + +type SubscriptionContext = { + onOpen?: OpenEvent; + onMessage: MessageListeners; + onError?: ErrorEvent; +}; + +type SubscriptionArgument = MessageListeners | SubscriptionContext; + +type Cancel = () => void; const sessionId = ( Date.now().toString(36) + @@ -33,28 +63,34 @@ const extractXsrfToken = () => { return extractXsrfTokenFromCookie(document.cookie); }; -const applyFetchOptions: (p: RequestInit) => RequestInit = o => { - if (!o.headers) { - o.headers = {}; - } - - // @ts-ignore We are sure that here we only get headers of type Record - const headers: Record = o.headers; - headers["Cache"] = "no-cache"; - // identify the request as ajax request - headers["X-Requested-With"] = "XMLHttpRequest"; - // identify the web interface - headers["X-SCM-Client"] = "WUI"; - // identify the window session - headers["X-SCM-Session-ID"] = sessionId +const createRequestHeaders = () => { + const headers: { [key: string]: string } = { + // disable caching for now + Cache: "no-cache", + // identify the request as ajax request + "X-Requested-With": "XMLHttpRequest", + // identify the web interface + "X-SCM-Client": "WUI", + // identify the window session + "X-SCM-Session-ID": sessionId + }; const xsrf = extractXsrfToken(); if (xsrf) { headers["X-XSRF-Token"] = xsrf; } + return headers; +}; +const applyFetchOptions: (p: RequestInit) => RequestInit = o => { + if (o.headers) { + o.headers = { + ...createRequestHeaders() + }; + } else { + o.headers = createRequestHeaders(); + } o.credentials = "same-origin"; - o.headers = headers; return o; }; @@ -174,12 +210,39 @@ class ApiClient { if (!options.headers) { options.headers = {}; } - // @ts-ignore We are sure that here we only get headers of type Record + // @ts-ignore We are sure that here we only get headers of type {[name:string]: string} options.headers["Content-Type"] = contentType; } return fetch(createUrl(url), options).then(handleFailure); } + + subscribe(url: string, argument: SubscriptionArgument): Cancel { + const es = new EventSourcePolyfill(createUrl(url), { + withCredentials: true, + headers: createRequestHeaders() + }); + + let listeners: MessageListeners; + // type guard, to identify that argument is of type SubscriptionContext + if ("onMessage" in argument) { + listeners = (argument as SubscriptionContext).onMessage; + if (argument.onError) { + es.onerror = argument.onError; + } + if (argument.onOpen) { + es.onopen = argument.onOpen; + } + } else { + listeners = argument; + } + + for (const type in listeners) { + es.addEventListener(type, listeners[type]); + } + + return es.close; + } } export const apiClient = new ApiClient(); diff --git a/yarn.lock b/yarn.lock index 35cfe2e580..58c39be6ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6539,6 +6539,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-source-polyfill@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/event-source-polyfill/-/event-source-polyfill-1.0.9.tgz#1fe3ebf8e3faddafd4fc237424f5e5ab2706b6d0" + integrity sha512-+x0BMKTYwZcmGmlkHK0GsXkX1+otfEwqu3QitN0wmWuHaZniw3HeIx1k5OjWX3JUHQHlPS4yONol6eokS1ZAWg== + eventemitter3@^3.1.0: version "3.1.2" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"