Merge pull request #24861 from alexef/new-events-implementation
fix: move from event-source-polyfill to fetch-event-source
This commit is contained in:
@@ -29,7 +29,6 @@ import {
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
compatWrapper,
|
||||
@@ -55,14 +54,12 @@ const techDocsStorageApi = createApiExtension({
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -17,16 +17,15 @@
|
||||
import { UrlPatternDiscovery } from '@backstage/core-app-api';
|
||||
import { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { NotFoundError } from '@backstage/errors';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
import { MockConfigApi, MockFetchApi } from '@backstage/test-utils';
|
||||
import { TechDocsStorageClient } from './client';
|
||||
|
||||
const MockedEventSource = EventSourcePolyfill as jest.MockedClass<
|
||||
typeof EventSourcePolyfill
|
||||
jest.mock('@microsoft/fetch-event-source');
|
||||
const mockFetchEventSource = fetchEventSource as jest.MockedFunction<
|
||||
typeof fetchEventSource
|
||||
>;
|
||||
|
||||
jest.mock('event-source-polyfill');
|
||||
|
||||
const mockEntity = {
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
@@ -51,7 +50,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -72,7 +70,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -84,71 +81,50 @@ describe('TechDocsStorageClient', () => {
|
||||
});
|
||||
|
||||
describe('syncEntityDocs', () => {
|
||||
it('should create eventsource without headers', async () => {
|
||||
it('should create eventsource with fetch', async () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await storageApi.syncEntityDocs(mockEntity);
|
||||
|
||||
expect(MockedEventSource).toHaveBeenCalledWith(
|
||||
expect(mockFetchEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
|
||||
{ withCredentials: true, headers: {} },
|
||||
);
|
||||
});
|
||||
|
||||
it('should create eventsource with headers', async () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
{
|
||||
fetch: fetchApi.fetch,
|
||||
onerror: expect.any(Function),
|
||||
onmessage: expect.any(Function),
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({ token: 'token' });
|
||||
await storageApi.syncEntityDocs(mockEntity);
|
||||
|
||||
expect(MockedEventSource).toHaveBeenCalledWith(
|
||||
'http://backstage:9191/api/techdocs/sync/default/Component/test-component',
|
||||
{ withCredentials: true, headers: { Authorization: 'Bearer token' } },
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve to cached', async () => {
|
||||
const myFetchApi = new MockFetchApi({
|
||||
injectIdentityAuth: { identityApi },
|
||||
});
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
fetchApi: myFetchApi,
|
||||
});
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
|
||||
@@ -160,17 +136,15 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": true}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' });
|
||||
});
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual(
|
||||
@@ -182,21 +156,18 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
MockedEventSource.prototype.addEventListener.mockImplementation(
|
||||
(type, fn) => {
|
||||
if (type === 'log' && typeof fn === 'function') {
|
||||
fn({ data: '"A log message"' } as any);
|
||||
}
|
||||
|
||||
if (type === 'finish' && typeof fn === 'function') {
|
||||
fn({ data: '{"updated": false}' } as any);
|
||||
}
|
||||
},
|
||||
);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onopen, onmessage } = options;
|
||||
await Promise.resolve();
|
||||
await onopen?.({ ok: true } as Response);
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'log', data: '"A log message"' });
|
||||
await Promise.resolve();
|
||||
onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' });
|
||||
});
|
||||
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const logHandler = jest.fn();
|
||||
@@ -212,25 +183,18 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onerror } = options;
|
||||
onerror?.(new NotFoundError('Some not found warning'));
|
||||
});
|
||||
|
||||
// we await later after we emitted the error
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const promise = storageApi.syncEntityDocs(mockEntity).then();
|
||||
|
||||
// flush the event loop
|
||||
await new Promise(r => setTimeout(r));
|
||||
|
||||
const instance = MockedEventSource.mock
|
||||
.instances[0] as jest.Mocked<EventSource>;
|
||||
|
||||
instance.onerror?.({
|
||||
status: 404,
|
||||
message: 'Some not found warning',
|
||||
} as any);
|
||||
|
||||
await expect(promise).rejects.toThrow(NotFoundError);
|
||||
await expect(promise).rejects.toThrow('Some not found warning');
|
||||
});
|
||||
@@ -239,7 +203,6 @@ describe('TechDocsStorageClient', () => {
|
||||
const storageApi = new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
});
|
||||
|
||||
@@ -247,16 +210,10 @@ describe('TechDocsStorageClient', () => {
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const promise = storageApi.syncEntityDocs(mockEntity).then();
|
||||
|
||||
// flush the event loop
|
||||
await new Promise(r => setTimeout(r));
|
||||
|
||||
const instance = MockedEventSource.mock
|
||||
.instances[0] as jest.Mocked<EventSource>;
|
||||
|
||||
instance.onerror?.({
|
||||
type: 'error',
|
||||
data: 'Some other error',
|
||||
} as any);
|
||||
mockFetchEventSource.mockImplementation(async (_url, options) => {
|
||||
const { onerror } = options;
|
||||
onerror?.(new Error('Some other error'));
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow(Error);
|
||||
await expect(promise).rejects.toThrow('Some other error');
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
TechDocsMetadata,
|
||||
TechDocsStorageApi,
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
||||
|
||||
/**
|
||||
* API to talk to `techdocs-backend`.
|
||||
@@ -124,18 +124,17 @@ export class TechDocsClient implements TechDocsApi {
|
||||
export class TechDocsStorageClient implements TechDocsStorageApi {
|
||||
public configApi: Config;
|
||||
public discoveryApi: DiscoveryApi;
|
||||
public identityApi: IdentityApi;
|
||||
private fetchApi: FetchApi;
|
||||
|
||||
constructor(options: {
|
||||
configApi: Config;
|
||||
discoveryApi: DiscoveryApi;
|
||||
identityApi: IdentityApi;
|
||||
fetchApi: FetchApi;
|
||||
/** @deprecated identityApi is not needed any more */
|
||||
identityApi?: IdentityApi;
|
||||
}) {
|
||||
this.configApi = options.configApi;
|
||||
this.discoveryApi = options.discoveryApi;
|
||||
this.identityApi = options.identityApi;
|
||||
this.fetchApi = options.fetchApi;
|
||||
}
|
||||
|
||||
@@ -213,47 +212,32 @@ export class TechDocsStorageClient implements TechDocsStorageApi {
|
||||
|
||||
const apiOrigin = await this.getApiOrigin();
|
||||
const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`;
|
||||
const { token } = await this.identityApi.getCredentials();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// Polyfill is used to add support for custom headers and auth
|
||||
const source = new EventSourcePolyfill(url, {
|
||||
withCredentials: true,
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
|
||||
source.addEventListener('log', (e: any) => {
|
||||
if (e.data) {
|
||||
logHandler(JSON.parse(e.data));
|
||||
}
|
||||
});
|
||||
|
||||
source.addEventListener('finish', (e: any) => {
|
||||
let updated: boolean = false;
|
||||
|
||||
if (e.data) {
|
||||
({ updated } = JSON.parse(e.data));
|
||||
}
|
||||
|
||||
resolve(updated ? 'updated' : 'cached');
|
||||
});
|
||||
|
||||
source.onerror = (e: any) => {
|
||||
source.close();
|
||||
|
||||
switch (e.status) {
|
||||
// the endpoint returned a 404 status
|
||||
case 404:
|
||||
reject(new NotFoundError(e.message));
|
||||
return;
|
||||
|
||||
// also handles the event-stream close. the reject is ignored if the Promise was already
|
||||
// resolved by a finish event.
|
||||
default:
|
||||
const ctrl = new AbortController();
|
||||
fetchEventSource(url, {
|
||||
fetch: this.fetchApi.fetch,
|
||||
signal: ctrl.signal,
|
||||
onmessage(e: any) {
|
||||
if (e.event === 'log') {
|
||||
if (e.data) {
|
||||
logHandler(JSON.parse(e.data));
|
||||
}
|
||||
} else if (e.event === 'finish') {
|
||||
let updated: boolean = false;
|
||||
if (e.data) {
|
||||
({ updated } = JSON.parse(e.data));
|
||||
}
|
||||
resolve(updated ? 'updated' : 'cached');
|
||||
} else if (e.event === 'error') {
|
||||
reject(new Error(e.data));
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
onerror(err) {
|
||||
ctrl.abort();
|
||||
reject(err);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare module 'event-source-polyfill' {
|
||||
export class EventSourcePolyfill extends EventSource {
|
||||
constructor(
|
||||
url: string,
|
||||
options?: {
|
||||
withCredentials?: boolean;
|
||||
headers?: HeadersInit;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
createRoutableExtension,
|
||||
discoveryApiRef,
|
||||
fetchApiRef,
|
||||
identityApiRef,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import {
|
||||
createSearchResultListItemExtension,
|
||||
@@ -52,14 +51,12 @@ export const techdocsPlugin = createPlugin({
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user