-
Notifications
You must be signed in to change notification settings - Fork 159
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
rewrite SSEClient and implement postprocessing-finished event management
- Loading branch information
1 parent
e86b313
commit e7c35f6
Showing
15 changed files
with
359 additions
and
133 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
changelog/unreleased/enhancement-handle-postprocessing-state-via-sse
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
Enhancement: Handle postprocessing state via Server Sent Events | ||
|
||
We've added the functionality to listen for events from the server that update the postprocessing state, | ||
this allows the user to see if the postprocessing on a file is finished, without reloading the UI. | ||
|
||
https://github.com/owncloud/web/pull/9771 | ||
https://github.com/owncloud/web/issues/9769 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { fetchEventSource, FetchEventSourceInit } from '@microsoft/fetch-event-source' | ||
|
||
export enum MESSAGE_TYPE { | ||
NOTIFICATION = 'userlog-notification', | ||
POSTPROCESSING_FINISHED = 'postprocessing-finished' | ||
} | ||
|
||
class RetriableError extends Error { | ||
name = 'RetriableError' | ||
} | ||
|
||
const RECONNECT_RANDOM_OFFSET = 15000 | ||
|
||
export class SSEAdapter implements EventSource { | ||
url: string | ||
private fetchOptions: FetchEventSourceInit | ||
private abortController: AbortController | ||
private eventListenerMap: Record<string, ((event: MessageEvent) => any)[]> | ||
|
||
readonly readyState: number | ||
readonly withCredentials: boolean | ||
|
||
readonly CONNECTING: 0 | ||
readonly OPEN: 1 | ||
readonly CLOSED: 2 | ||
|
||
onerror: ((this: EventSource, ev: Event) => any) | null | ||
onmessage: ((this: EventSource, ev: MessageEvent) => any) | null | ||
onopen: ((this: EventSource, ev: Event) => any) | null | ||
|
||
constructor(url: string, fetchOptions: FetchEventSourceInit) { | ||
this.url = url | ||
this.fetchOptions = fetchOptions | ||
this.abortController = new AbortController() | ||
this.eventListenerMap = {} | ||
this.connect() | ||
} | ||
|
||
private connect() { | ||
return fetchEventSource(this.url, { | ||
openWhenHidden: true, | ||
signal: this.abortController.signal, | ||
fetch: this.fetchProvider.bind(this), | ||
onopen: async () => { | ||
const event = new Event('open') | ||
this.onopen?.bind(this)(event) | ||
}, | ||
onmessage: (msg) => { | ||
const event = new MessageEvent('message', { data: msg.data }) | ||
this.onmessage?.bind(this)(event) | ||
|
||
const type = msg.event | ||
const eventListeners = this.eventListenerMap[type] | ||
eventListeners?.forEach((l) => l(event)) | ||
}, | ||
onclose: () => { | ||
throw new RetriableError() | ||
}, | ||
onerror: (err) => { | ||
console.error(err) | ||
const event = new CustomEvent('error', { detail: err }) | ||
this.onerror?.bind(this)(event) | ||
|
||
/* | ||
* Try to reconnect after 30 seconds plus random time in seconds. | ||
* This prevents all clients try to reconnect concurrent on server error, to reduce load. | ||
*/ | ||
return 30000 + Math.floor(Math.random() * RECONNECT_RANDOM_OFFSET) | ||
} | ||
}) | ||
} | ||
|
||
private fetchProvider(...args) { | ||
let [resource, config] = args | ||
config = { ...config, ...this.fetchOptions } | ||
return window.fetch(resource, config) | ||
} | ||
|
||
close() { | ||
this.abortController.abort('closed') | ||
} | ||
|
||
addEventListener(type: string, listener: (this: EventSource, event: MessageEvent) => any): void { | ||
this.eventListenerMap[type] = this.eventListenerMap[type] || [] | ||
this.eventListenerMap[type].push(listener) | ||
} | ||
|
||
removeEventListener( | ||
type: string, | ||
listener: (this: EventSource, event: MessageEvent) => any | ||
): void { | ||
this.eventListenerMap[type] = this.eventListenerMap[type]?.filter((func) => func !== listener) | ||
} | ||
|
||
dispatchEvent(event: Event): boolean { | ||
throw new Error('Method not implemented.') | ||
} | ||
|
||
updateAccessToken(token: string) { | ||
this.fetchOptions.headers['Authorization'] = `Bearer ${token}` | ||
} | ||
|
||
updateLanguage(language: string) { | ||
this.fetchOptions.headers['Accept-Language'] = language | ||
|
||
// Force reconnect, to make the language change effect instantly | ||
this.close() | ||
this.connect() | ||
} | ||
} | ||
|
||
let eventSource: SSEAdapter = null | ||
|
||
export const sse = (baseURI: string, fetchOptions: FetchEventSourceInit): EventSource => { | ||
if (!eventSource) { | ||
eventSource = new SSEAdapter( | ||
new URL('ocs/v2.php/apps/notifications/api/v1/notifications/sse', baseURI).href, | ||
fetchOptions | ||
) | ||
} | ||
|
||
return eventSource | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
const fetchEventSourceMock = jest.fn() | ||
jest.mock('@microsoft/fetch-event-source', () => ({ | ||
fetchEventSource: fetchEventSourceMock | ||
})) | ||
|
||
import { SSEAdapter, sse, MESSAGE_TYPE, RetriableError } from '../../src/sse' | ||
|
||
const url = 'https://owncloud.test/' | ||
describe('SSEAdapter', () => { | ||
let mockFetch | ||
|
||
beforeEach(() => { | ||
mockFetch = jest.fn() | ||
|
||
// Mock fetchEventSource and window.fetch | ||
|
||
global.window.fetch = mockFetch | ||
}) | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks() | ||
}) | ||
|
||
test('it should initialize the SSEAdapter', () => { | ||
const fetchOptions = { method: 'GET' } | ||
|
||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
expect(sseAdapter.url).toBe(url) | ||
expect(sseAdapter.fetchOptions).toBe(fetchOptions) | ||
expect(sseAdapter.readyState).toBe(sseAdapter.CONNECTING) | ||
}) | ||
|
||
test('it should call connect and set up event listeners', () => { | ||
const fetchOptions = { method: 'GET' } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
expect(fetchEventSourceMock).toHaveBeenCalledWith(url, expect.any(Object)) | ||
expect(fetchEventSourceMock.mock.calls[0][1].onopen).toEqual(expect.any(Function)) | ||
|
||
fetchEventSourceMock.mock.calls[0][1].onopen() | ||
|
||
expect(sseAdapter.readyState).toBe(sseAdapter.OPEN) | ||
}) | ||
|
||
test('it should handle onmessage events', () => { | ||
const fetchOptions = { method: 'GET' } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
const message = { data: 'Message data', event: MESSAGE_TYPE.NOTIFICATION } | ||
|
||
const messageListener = jest.fn() | ||
sseAdapter.addEventListener(MESSAGE_TYPE.NOTIFICATION, messageListener) | ||
|
||
fetchEventSourceMock.mock.calls[0][1].onmessage(message) | ||
|
||
expect(messageListener).toHaveBeenCalledWith(expect.any(Object)) | ||
}) | ||
|
||
test('it should handle onclose events and throw RetriableError', () => { | ||
const fetchOptions = { method: 'GET' } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
expect(() => { | ||
// Simulate onclose | ||
fetchEventSourceMock.mock.calls[0][1].onclose() | ||
}).toThrow(RetriableError) | ||
}) | ||
|
||
test('it should call fetchProvider with fetch options', () => { | ||
const fetchOptions = { headers: { Authorization: 'Bearer xy' } } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
sseAdapter.fetchProvider(url, fetchOptions) | ||
|
||
expect(mockFetch).toHaveBeenCalledWith(url, { ...fetchOptions }) | ||
}) | ||
|
||
test('it should update the access token in fetch options', () => { | ||
const fetchOptions = { headers: { Authorization: 'Bearer xy' } } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
const token = 'new-token' | ||
sseAdapter.updateAccessToken(token) | ||
|
||
expect(sseAdapter.fetchOptions.headers.Authorization).toBe(`Bearer ${token}`) | ||
}) | ||
|
||
test('it should close the SSEAdapter', () => { | ||
const fetchOptions = { method: 'GET' } | ||
const sseAdapter = new SSEAdapter(url, fetchOptions) | ||
|
||
sseAdapter.close() | ||
|
||
expect(sseAdapter.readyState).toBe(sseAdapter.CLOSED) | ||
}) | ||
}) | ||
|
||
describe('sse', () => { | ||
test('it should create and return an SSEAdapter instance', () => { | ||
const fetchOptions = { method: 'GET' } | ||
const eventSource = sse(url, fetchOptions) | ||
|
||
expect(eventSource).toBeInstanceOf(SSEAdapter) | ||
expect(eventSource.url).toBe(`${url}ocs/v2.php/apps/notifications/api/v1/notifications/sse`) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
79 changes: 0 additions & 79 deletions
79
packages/web-pkg/src/composables/sse/useServerSentEvents.ts
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.