Skip to content

Commit bdf7823

Browse files
[SSE] Fix EventSource streams (elastic#213151)
## Summary Resolves elastic#212919 We noticed that setting the header `'Content-Type': 'text/event-stream',` didn't work as the browser's native EventSource implementation. ```JS return res.ok({ headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', }, body: observableIntoEventSourceStream(events$ as unknown as Observable<ServerSentEvent>, { signal: abortController.signal, logger, }), }); ``` The reason, apparently, is that we need to flush the compressor's buffer negotiated in the HTTP request. ### How to test it: Run Kibana with examples `yarn start --no-base-path --run-examples --http2` and open the SSE example app in Kibana. You should see a clock updating every second in the UI (the clock is coming from the server). --------- Co-authored-by: kibanamachine <[email protected]>
1 parent c348bd1 commit bdf7823

18 files changed

+389
-12
lines changed

.github/CODEOWNERS

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ examples/routing_example @elastic/kibana-core
3434
examples/screenshot_mode_example @elastic/appex-sharedux
3535
examples/search_examples @elastic/kibana-data-discovery
3636
examples/share_examples @elastic/appex-sharedux
37+
examples/sse_example @elastic/kibana-core
3738
examples/state_containers_examples @elastic/appex-sharedux
3839
examples/ui_action_examples @elastic/appex-sharedux
3940
examples/ui_actions_explorer @elastic/appex-sharedux

examples/sse_example/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# SSE Example
2+
3+
This plugin's goal is to demonstrate how to implement Server-Sent Events.

examples/sse_example/common/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
export const PLUGIN_ID = 'sseExample';
11+
export const PLUGIN_NAME = 'SSE Example';

examples/sse_example/kibana.jsonc

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "plugin",
3+
"id": "@kbn/sse-example-plugin",
4+
"owner": "@elastic/kibana-core",
5+
"description": "Plugin that shows how to implement Server-Sent Events.",
6+
"plugin": {
7+
"id": "sseExample",
8+
"server": true,
9+
"browser": true,
10+
"requiredPlugins": ["developerExamples"],
11+
"optionalPlugins": []
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React from 'react';
11+
import ReactDOM from 'react-dom';
12+
import { AppMountParameters, CoreStart } from '@kbn/core/public';
13+
import { KibanaRenderContextProvider } from '@kbn/react-kibana-context-render';
14+
import { SseExampleApp } from './components/app';
15+
16+
export const renderApp = (coreStart: CoreStart, { element }: AppMountParameters) => {
17+
const { http } = coreStart;
18+
ReactDOM.render(
19+
<KibanaRenderContextProvider {...coreStart}>
20+
<SseExampleApp http={http} />
21+
</KibanaRenderContextProvider>,
22+
element
23+
);
24+
25+
return () => ReactDOM.unmountComponentAtNode(element);
26+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import React, { useMemo } from 'react';
11+
import { EuiPageTemplate, EuiTitle } from '@elastic/eui';
12+
import type { CoreStart } from '@kbn/core/public';
13+
14+
import useObservable from 'react-use/lib/useObservable';
15+
import { defer } from 'rxjs';
16+
import { httpResponseIntoObservable } from '@kbn/sse-utils-client';
17+
import { PLUGIN_NAME } from '../../common';
18+
19+
interface FeatureFlagsExampleAppDeps {
20+
http: CoreStart['http'];
21+
}
22+
23+
export const SseExampleApp = ({ http }: FeatureFlagsExampleAppDeps) => {
24+
const sseResponse$ = useMemo(() => {
25+
return defer(() =>
26+
http.get('/internal/sse_examples/clock', {
27+
asResponse: true,
28+
rawResponse: true,
29+
version: '1',
30+
})
31+
).pipe(httpResponseIntoObservable<{ message: string; type: 'clock' }>());
32+
}, [http]);
33+
34+
const sseResponse = useObservable(sseResponse$, { message: 'Initial value', type: 'clock' });
35+
36+
return (
37+
<>
38+
<EuiPageTemplate>
39+
<EuiPageTemplate.Header>
40+
<EuiTitle size="l">
41+
<h1>{PLUGIN_NAME}</h1>
42+
</EuiTitle>
43+
</EuiPageTemplate.Header>
44+
<EuiPageTemplate.Section>
45+
<h2>{sseResponse.message}</h2>
46+
</EuiPageTemplate.Section>
47+
</EuiPageTemplate>
48+
</>
49+
);
50+
};

examples/sse_example/public/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { SseExamplePlugin } from './plugin';
11+
12+
export function plugin() {
13+
return new SseExamplePlugin();
14+
}

examples/sse_example/public/plugin.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { AppMountParameters, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
11+
import { AppPluginSetupDependencies } from './types';
12+
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
13+
14+
export class SseExamplePlugin implements Plugin {
15+
public setup(core: CoreSetup, deps: AppPluginSetupDependencies) {
16+
// Register an application into the side navigation menu
17+
core.application.register({
18+
id: PLUGIN_ID,
19+
title: PLUGIN_NAME,
20+
async mount(params: AppMountParameters) {
21+
// Load application bundle
22+
const { renderApp } = await import('./application');
23+
// Get start services as specified in kibana.json
24+
const [coreStart] = await core.getStartServices();
25+
// Render the application
26+
return renderApp(coreStart, params);
27+
},
28+
});
29+
30+
deps.developerExamples.register({
31+
appId: PLUGIN_ID,
32+
title: PLUGIN_NAME,
33+
description: 'Plugin that shows how to make use of the feature flags core service.',
34+
});
35+
}
36+
37+
public start(core: CoreStart) {}
38+
39+
public stop() {}
40+
}

examples/sse_example/public/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { DeveloperExamplesSetup } from '@kbn/developer-examples-plugin/public';
11+
12+
export interface AppPluginSetupDependencies {
13+
developerExamples: DeveloperExamplesSetup;
14+
}

examples/sse_example/server/index.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { PluginInitializerContext } from '@kbn/core-plugins-server';
11+
12+
export async function plugin(initializerContext: PluginInitializerContext) {
13+
const { SseExamplePlugin } = await import('./plugin');
14+
return new SseExamplePlugin(initializerContext);
15+
}

examples/sse_example/server/plugin.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from '@kbn/core/server';
11+
12+
import { defineRoutes } from './routes';
13+
14+
export class SseExamplePlugin implements Plugin {
15+
private readonly logger: Logger;
16+
17+
constructor(initializerContext: PluginInitializerContext) {
18+
this.logger = initializerContext.logger.get();
19+
}
20+
21+
public setup(core: CoreSetup) {
22+
const router = core.http.createRouter();
23+
24+
// Register server side APIs
25+
defineRoutes(router, this.logger);
26+
}
27+
28+
public start() {}
29+
30+
public stop() {}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import type { IRouter, Logger } from '@kbn/core/server';
11+
import { Observable, defer, map, timer } from 'rxjs';
12+
import { observableIntoEventSourceStream } from '@kbn/sse-utils-server';
13+
import { ServerSentEvent } from '@kbn/sse-utils/src/events';
14+
15+
export function defineRoutes(router: IRouter, logger: Logger) {
16+
router.versioned
17+
.get({
18+
path: '/internal/sse_examples/clock',
19+
access: 'internal',
20+
})
21+
.addVersion(
22+
{
23+
version: '1',
24+
validate: false,
25+
},
26+
async (context, request, response) => {
27+
const abortController = new AbortController();
28+
request.events.aborted$.subscribe(() => {
29+
abortController.abort();
30+
});
31+
32+
const events$: Observable<ServerSentEvent> = defer(() => timer(0, 1000)).pipe(
33+
map(() => ({ type: 'clock', message: `Hi! It's ${new Date().toLocaleTimeString()}!` }))
34+
);
35+
36+
return response.ok({
37+
headers: {
38+
'Content-Type': 'text/event-stream',
39+
},
40+
body: observableIntoEventSourceStream(events$, {
41+
signal: abortController.signal,
42+
logger,
43+
}),
44+
});
45+
}
46+
);
47+
}

examples/sse_example/tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "target/types"
5+
},
6+
"include": [
7+
"index.ts",
8+
"common/**/*.ts",
9+
"public/**/*.ts",
10+
"public/**/*.tsx",
11+
"server/**/*.ts",
12+
"../../typings/**/*"
13+
],
14+
"exclude": ["target/**/*"],
15+
"kbn_references": [
16+
"@kbn/core",
17+
"@kbn/core-plugins-server",
18+
"@kbn/developer-examples-plugin",
19+
"@kbn/react-kibana-context-render",
20+
"@kbn/sse-utils-client",
21+
"@kbn/sse-utils-server",
22+
"@kbn/sse-utils",
23+
]
24+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,7 @@
925925
"@kbn/sort-predicates": "link:src/platform/packages/shared/kbn-sort-predicates",
926926
"@kbn/spaces-plugin": "link:x-pack/platform/plugins/shared/spaces",
927927
"@kbn/spaces-test-plugin": "link:x-pack/test/spaces_api_integration/common/plugins/spaces_test_plugin",
928+
"@kbn/sse-example-plugin": "link:examples/sse_example",
928929
"@kbn/sse-utils": "link:src/platform/packages/shared/kbn-sse-utils",
929930
"@kbn/sse-utils-client": "link:src/platform/packages/shared/kbn-sse-utils-client",
930931
"@kbn/sse-utils-server": "link:src/platform/packages/shared/kbn-sse-utils-server",

0 commit comments

Comments
 (0)