mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Integrate new platform (core) server side into Kibana (#20661)
Co-authored-by: Kim Joar Bekkelund <kjbekkelund@gmail.com> Co-authored-by: archana <archanid@users.noreply.github.com> Co-authored-by: Spencer <spalger@users.noreply.github.com> Co-authored-by: Court Ewing <court@epixa.com>
This commit is contained in:
parent
5e0cddcc9e
commit
f88d0b92a2
242 changed files with 18697 additions and 606 deletions
|
@ -15,6 +15,7 @@ bower_components
|
|||
/src/core_plugins/console/public/tests/webpackShims
|
||||
/src/ui/public/utils/decode_geo_hash.js
|
||||
/src/core_plugins/timelion/public/webpackShims/jquery.flot.*
|
||||
/src/core/lib/kbn_internal_native_observable
|
||||
/packages/*/target
|
||||
/packages/eslint-config-kibana
|
||||
/packages/eslint-plugin-kibana-custom
|
||||
|
|
15
package.json
15
package.json
|
@ -129,8 +129,10 @@
|
|||
"glob-all": "3.0.1",
|
||||
"good-squeeze": "2.1.0",
|
||||
"h2o2": "5.1.1",
|
||||
"h2o2-latest": "npm:h2o2@8.1.2",
|
||||
"handlebars": "4.0.5",
|
||||
"hapi": "14.2.0",
|
||||
"hapi-latest": "npm:hapi@17.5.0",
|
||||
"hjson": "3.1.0",
|
||||
"http-proxy-agent": "^2.1.0",
|
||||
"https-proxy-agent": "^2.2.1",
|
||||
|
@ -193,12 +195,14 @@
|
|||
"script-loader": "0.7.2",
|
||||
"semver": "^5.5.0",
|
||||
"style-loader": "0.19.0",
|
||||
"symbol-observable": "^1.2.0",
|
||||
"tar": "2.2.0",
|
||||
"tinygradient": "0.3.0",
|
||||
"tinymath": "0.2.1",
|
||||
"topojson-client": "3.0.0",
|
||||
"trunc-html": "1.0.2",
|
||||
"trunc-text": "1.0.2",
|
||||
"type-detect": "^4.0.8",
|
||||
"uglifyjs-webpack-plugin": "0.4.6",
|
||||
"ui-select": "0.19.6",
|
||||
"url-loader": "0.5.9",
|
||||
|
@ -228,21 +232,31 @@
|
|||
"@types/angular": "^1.6.45",
|
||||
"@types/babel-core": "^6.25.5",
|
||||
"@types/bluebird": "^3.1.1",
|
||||
"@types/chance": "^1.0.0",
|
||||
"@types/classnames": "^2.2.3",
|
||||
"@types/eslint": "^4.16.2",
|
||||
"@types/execa": "^0.9.0",
|
||||
"@types/getopts": "^2.0.0",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/hapi-latest": "npm:@types/hapi@17.0.12",
|
||||
"@types/has-ansi": "^3.0.0",
|
||||
"@types/jest": "^22.2.3",
|
||||
"@types/joi": "^10.4.4",
|
||||
"@types/jquery": "3.3.1",
|
||||
"@types/js-yaml": "^3.11.1",
|
||||
"@types/listr": "^0.13.0",
|
||||
"@types/lodash": "^3.10.1",
|
||||
"@types/minimatch": "^2.0.29",
|
||||
"@types/node": "^8.10.20",
|
||||
"@types/prop-types": "^15.5.3",
|
||||
"@types/react": "^16.3.14",
|
||||
"@types/react-dom": "^16.0.5",
|
||||
"@types/redux": "^3.6.31",
|
||||
"@types/redux-actions": "^2.2.1",
|
||||
"@types/sinon": "^5.0.0",
|
||||
"@types/strip-ansi": "^3.0.0",
|
||||
"@types/supertest": "^2.0.4",
|
||||
"@types/type-detect": "^4.0.1",
|
||||
"angular-mocks": "1.4.7",
|
||||
"babel-eslint": "8.1.2",
|
||||
"babel-jest": "^22.4.3",
|
||||
|
@ -282,6 +296,7 @@
|
|||
"grunt-run": "0.7.0",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-sourcemaps": "1.7.3",
|
||||
"has-ansi": "^3.0.0",
|
||||
"husky": "0.8.1",
|
||||
"image-diff": "1.6.0",
|
||||
"istanbul-instrumenter-loader": "3.0.0",
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { notFound } from 'boom';
|
||||
import { map, sample } from 'lodash';
|
||||
import { map as promiseMap, fromNode } from 'bluebird';
|
||||
import { Agent as HttpsAgent } from 'https';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
import { setupConnection } from '../../server/http/setup_connection';
|
||||
import { registerHapiPlugins } from '../../server/http/register_hapi_plugins';
|
||||
import { setupLogging } from '../../server/logging';
|
||||
|
||||
const alphabet = 'abcdefghijklmnopqrztuvwxyz'.split('');
|
||||
|
||||
export default class BasePathProxy {
|
||||
constructor(clusterManager, config) {
|
||||
this.clusterManager = clusterManager;
|
||||
this.server = new Server();
|
||||
|
||||
this.targetPort = config.get('dev.basePathProxyTarget');
|
||||
this.basePath = config.get('server.basePath');
|
||||
|
||||
const sslEnabled = config.get('server.ssl.enabled');
|
||||
if (sslEnabled) {
|
||||
this.proxyAgent = new HttpsAgent({
|
||||
key: readFileSync(config.get('server.ssl.key')),
|
||||
passphrase: config.get('server.ssl.keyPassphrase'),
|
||||
cert: readFileSync(config.get('server.ssl.certificate')),
|
||||
ca: map(config.get('server.ssl.certificateAuthorities'), readFileSync),
|
||||
rejectUnauthorized: false
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.basePath) {
|
||||
this.basePath = `/${sample(alphabet, 3).join('')}`;
|
||||
config.set('server.basePath', this.basePath);
|
||||
}
|
||||
|
||||
const ONE_GIGABYTE = 1024 * 1024 * 1024;
|
||||
config.set('server.maxPayloadBytes', ONE_GIGABYTE);
|
||||
|
||||
setupLogging(this.server, config);
|
||||
setupConnection(this.server, config);
|
||||
registerHapiPlugins(this.server, config);
|
||||
|
||||
this.setupRoutes();
|
||||
}
|
||||
|
||||
setupRoutes() {
|
||||
const { clusterManager, server, basePath, targetPort } = this;
|
||||
|
||||
server.route({
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
handler(req, reply) {
|
||||
return reply.redirect(basePath);
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: '*',
|
||||
path: `${basePath}/{kbnPath*}`,
|
||||
config: {
|
||||
pre: [
|
||||
(req, reply) => {
|
||||
promiseMap(clusterManager.workers, worker => {
|
||||
if (worker.type === 'server' && !worker.listening && !worker.crashed) {
|
||||
return fromNode(cb => {
|
||||
const done = () => {
|
||||
worker.removeListener('listening', done);
|
||||
worker.removeListener('crashed', done);
|
||||
cb();
|
||||
};
|
||||
|
||||
worker.on('listening', done);
|
||||
worker.on('crashed', done);
|
||||
});
|
||||
}
|
||||
})
|
||||
.return(undefined)
|
||||
.nodeify(reply);
|
||||
}
|
||||
],
|
||||
},
|
||||
handler: {
|
||||
proxy: {
|
||||
passThrough: true,
|
||||
xforward: true,
|
||||
agent: this.proxyAgent,
|
||||
protocol: server.info.protocol,
|
||||
host: server.info.host,
|
||||
port: targetPort,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: '*',
|
||||
path: `/{oldBasePath}/{kbnPath*}`,
|
||||
handler(req, reply) {
|
||||
const { oldBasePath, kbnPath = '' } = req.params;
|
||||
|
||||
const isGet = req.method === 'get';
|
||||
const isBasePath = oldBasePath.length === 3;
|
||||
const isApp = kbnPath.startsWith('app/');
|
||||
const isKnownShortPath = ['login', 'logout', 'status'].includes(kbnPath);
|
||||
|
||||
if (isGet && isBasePath && (isApp || isKnownShortPath)) {
|
||||
return reply.redirect(`${basePath}/${kbnPath}`);
|
||||
}
|
||||
|
||||
return reply(notFound());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async listen() {
|
||||
await fromNode(cb => this.server.start(cb));
|
||||
this.server.log(['listening', 'info'], `basePath Proxy running at ${this.server.info.uri}${this.basePath}`);
|
||||
}
|
||||
|
||||
}
|
|
@ -22,9 +22,9 @@ import { debounce, invoke, bindAll, once, uniq } from 'lodash';
|
|||
|
||||
import Log from '../log';
|
||||
import Worker from './worker';
|
||||
import BasePathProxy from './base_path_proxy';
|
||||
import { Config } from '../../server/config/config';
|
||||
import { transformDeprecations } from '../../server/config/transform_deprecations';
|
||||
import { configureBasePathProxy } from './configure_base_path_proxy';
|
||||
|
||||
process.env.kbnWorkerType = 'managr';
|
||||
|
||||
|
@ -33,10 +33,14 @@ export default class ClusterManager {
|
|||
const transformedSettings = transformDeprecations(settings);
|
||||
const config = await Config.withDefaultSchema(transformedSettings);
|
||||
|
||||
return new ClusterManager(opts, config);
|
||||
const basePathProxy = opts.basePath
|
||||
? await configureBasePathProxy(config)
|
||||
: undefined;
|
||||
|
||||
return new ClusterManager(opts, config, basePathProxy);
|
||||
}
|
||||
|
||||
constructor(opts, config) {
|
||||
constructor(opts, config, basePathProxy) {
|
||||
this.log = new Log(opts.quiet, opts.silent);
|
||||
this.addedCount = 0;
|
||||
|
||||
|
@ -46,17 +50,17 @@ export default class ClusterManager {
|
|||
'--server.autoListen=false',
|
||||
];
|
||||
|
||||
if (opts.basePath) {
|
||||
this.basePathProxy = new BasePathProxy(this, config);
|
||||
if (basePathProxy) {
|
||||
this.basePathProxy = basePathProxy;
|
||||
|
||||
optimizerArgv.push(
|
||||
`--server.basePath=${this.basePathProxy.basePath}`,
|
||||
`--server.basePath=${this.basePathProxy.getBasePath()}`,
|
||||
'--server.rewriteBasePath=true',
|
||||
);
|
||||
|
||||
serverArgv.push(
|
||||
`--server.port=${this.basePathProxy.targetPort}`,
|
||||
`--server.basePath=${this.basePathProxy.basePath}`,
|
||||
`--server.port=${this.basePathProxy.getTargetPort()}`,
|
||||
`--server.basePath=${this.basePathProxy.getBasePath()}`,
|
||||
'--server.rewriteBasePath=true',
|
||||
);
|
||||
}
|
||||
|
@ -77,6 +81,12 @@ export default class ClusterManager {
|
|||
})
|
||||
];
|
||||
|
||||
if (basePathProxy) {
|
||||
// Pass server worker to the basepath proxy so that it can hold off the
|
||||
// proxying until server worker is ready.
|
||||
this.basePathProxy.serverWorker = this.server;
|
||||
}
|
||||
|
||||
// broker messages between workers
|
||||
this.workers.forEach((worker) => {
|
||||
worker.on('broadcast', (msg) => {
|
||||
|
@ -119,7 +129,7 @@ export default class ClusterManager {
|
|||
this.setupManualRestart();
|
||||
invoke(this.workers, 'start');
|
||||
if (this.basePathProxy) {
|
||||
this.basePathProxy.listen();
|
||||
this.basePathProxy.start();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
64
src/cli/cluster/configure_base_path_proxy.js
Normal file
64
src/cli/cluster/configure_base_path_proxy.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { createBasePathProxy } from '../../core';
|
||||
import { setupLogging } from '../../server/logging';
|
||||
|
||||
export async function configureBasePathProxy(config) {
|
||||
// New platform forwards all logs to the legacy platform so we need HapiJS server
|
||||
// here just for logging purposes and nothing else.
|
||||
const server = new Server();
|
||||
setupLogging(server, config);
|
||||
|
||||
const basePathProxy = createBasePathProxy({ server, config });
|
||||
|
||||
await basePathProxy.configure({
|
||||
shouldRedirectFromOldBasePath: path => {
|
||||
const isApp = path.startsWith('app/');
|
||||
const isKnownShortPath = ['login', 'logout', 'status'].includes(path);
|
||||
|
||||
return isApp || isKnownShortPath;
|
||||
},
|
||||
|
||||
blockUntil: () => {
|
||||
// Wait until `serverWorker either crashes or starts to listen.
|
||||
// The `serverWorker` property should be set by the ClusterManager
|
||||
// once it creates the worker.
|
||||
const serverWorker = basePathProxy.serverWorker;
|
||||
if (serverWorker.listening || serverWorker.crashed) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const done = () => {
|
||||
serverWorker.removeListener('listening', done);
|
||||
serverWorker.removeListener('crashed', done);
|
||||
|
||||
resolve();
|
||||
};
|
||||
|
||||
serverWorker.on('listening', done);
|
||||
serverWorker.on('crashed', done);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return basePathProxy;
|
||||
}
|
163
src/cli/cluster/configure_base_path_proxy.test.js
Normal file
163
src/cli/cluster/configure_base_path_proxy.test.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
jest.mock('../../core', () => ({
|
||||
createBasePathProxy: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../server/logging', () => ({
|
||||
setupLogging: jest.fn(),
|
||||
}));
|
||||
|
||||
import { Server } from 'hapi';
|
||||
import { createBasePathProxy as createBasePathProxyMock } from '../../core';
|
||||
import { setupLogging as setupLoggingMock } from '../../server/logging';
|
||||
import { configureBasePathProxy } from './configure_base_path_proxy';
|
||||
|
||||
describe('configureBasePathProxy()', () => {
|
||||
it('returns `BasePathProxy` instance.', async () => {
|
||||
const basePathProxyMock = { configure: jest.fn() };
|
||||
createBasePathProxyMock.mockReturnValue(basePathProxyMock);
|
||||
|
||||
const basePathProxy = await configureBasePathProxy({});
|
||||
|
||||
expect(basePathProxy).toBe(basePathProxyMock);
|
||||
});
|
||||
|
||||
it('correctly configures `BasePathProxy`.', async () => {
|
||||
const configMock = {};
|
||||
const basePathProxyMock = { configure: jest.fn() };
|
||||
createBasePathProxyMock.mockReturnValue(basePathProxyMock);
|
||||
|
||||
await configureBasePathProxy(configMock);
|
||||
|
||||
// Check that logging is configured with the right parameters.
|
||||
expect(setupLoggingMock).toHaveBeenCalledWith(
|
||||
expect.any(Server),
|
||||
configMock
|
||||
);
|
||||
|
||||
const [[server]] = setupLoggingMock.mock.calls;
|
||||
expect(createBasePathProxyMock).toHaveBeenCalledWith({
|
||||
config: configMock,
|
||||
server,
|
||||
});
|
||||
|
||||
expect(basePathProxyMock.configure).toHaveBeenCalledWith({
|
||||
shouldRedirectFromOldBasePath: expect.any(Function),
|
||||
blockUntil: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
describe('configured with the correct `shouldRedirectFromOldBasePath` and `blockUntil` functions.', async () => {
|
||||
let serverWorkerMock;
|
||||
let shouldRedirectFromOldBasePath;
|
||||
let blockUntil;
|
||||
beforeEach(async () => {
|
||||
serverWorkerMock = {
|
||||
listening: false,
|
||||
crashed: false,
|
||||
on: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
|
||||
const basePathProxyMock = {
|
||||
configure: jest.fn(),
|
||||
serverWorker: serverWorkerMock,
|
||||
};
|
||||
|
||||
createBasePathProxyMock.mockReturnValue(basePathProxyMock);
|
||||
|
||||
await configureBasePathProxy({});
|
||||
|
||||
[[{ blockUntil, shouldRedirectFromOldBasePath }]] = basePathProxyMock.configure.mock.calls;
|
||||
});
|
||||
|
||||
it('`shouldRedirectFromOldBasePath()` returns `false` for unknown paths.', async () => {
|
||||
expect(shouldRedirectFromOldBasePath('')).toBe(false);
|
||||
expect(shouldRedirectFromOldBasePath('some-path/')).toBe(false);
|
||||
expect(shouldRedirectFromOldBasePath('some-other-path')).toBe(false);
|
||||
});
|
||||
|
||||
it('`shouldRedirectFromOldBasePath()` returns `true` for `app` and other known paths.', async () => {
|
||||
expect(shouldRedirectFromOldBasePath('app/')).toBe(true);
|
||||
expect(shouldRedirectFromOldBasePath('login')).toBe(true);
|
||||
expect(shouldRedirectFromOldBasePath('logout')).toBe(true);
|
||||
expect(shouldRedirectFromOldBasePath('status')).toBe(true);
|
||||
});
|
||||
|
||||
it('`blockUntil()` resolves immediately if worker has already crashed.', async () => {
|
||||
serverWorkerMock.crashed = true;
|
||||
|
||||
await expect(blockUntil()).resolves.not.toBeDefined();
|
||||
expect(serverWorkerMock.on).not.toHaveBeenCalled();
|
||||
expect(serverWorkerMock.removeListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('`blockUntil()` resolves immediately if worker is already listening.', async () => {
|
||||
serverWorkerMock.listening = true;
|
||||
|
||||
await expect(blockUntil()).resolves.not.toBeDefined();
|
||||
expect(serverWorkerMock.on).not.toHaveBeenCalled();
|
||||
expect(serverWorkerMock.removeListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('`blockUntil()` resolves when worker crashes.', async () => {
|
||||
const blockUntilPromise = blockUntil();
|
||||
|
||||
expect(serverWorkerMock.on).toHaveBeenCalledTimes(2);
|
||||
expect(serverWorkerMock.on).toHaveBeenCalledWith(
|
||||
'crashed',
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
const [, [eventName, onCrashed]] = serverWorkerMock.on.mock.calls;
|
||||
// Check event name to make sure we call the right callback,
|
||||
// in Jest 23 we could use `toHaveBeenNthCalledWith` instead.
|
||||
expect(eventName).toBe('crashed');
|
||||
expect(serverWorkerMock.removeListener).not.toHaveBeenCalled();
|
||||
|
||||
onCrashed();
|
||||
await expect(blockUntilPromise).resolves.not.toBeDefined();
|
||||
|
||||
expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('`blockUntil()` resolves when worker starts listening.', async () => {
|
||||
const blockUntilPromise = blockUntil();
|
||||
|
||||
expect(serverWorkerMock.on).toHaveBeenCalledTimes(2);
|
||||
expect(serverWorkerMock.on).toHaveBeenCalledWith(
|
||||
'listening',
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
const [[eventName, onListening]] = serverWorkerMock.on.mock.calls;
|
||||
// Check event name to make sure we call the right callback,
|
||||
// in Jest 23 we could use `toHaveBeenNthCalledWith` instead.
|
||||
expect(eventName).toBe('listening');
|
||||
expect(serverWorkerMock.removeListener).not.toHaveBeenCalled();
|
||||
|
||||
onListening();
|
||||
await expect(blockUntilPromise).resolves.not.toBeDefined();
|
||||
|
||||
expect(serverWorkerMock.removeListener).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -13,6 +13,37 @@ Object {
|
|||
],
|
||||
"type": "log",
|
||||
},
|
||||
Object {
|
||||
"@timestamp": "## @timestamp ##",
|
||||
"message": "starting server :tada:",
|
||||
"pid": "## PID ##",
|
||||
"tags": Array [
|
||||
"info",
|
||||
"server",
|
||||
],
|
||||
"type": "log",
|
||||
},
|
||||
Object {
|
||||
"@timestamp": "## @timestamp ##",
|
||||
"message": "registering route handler for [/core]",
|
||||
"pid": "## PID ##",
|
||||
"tags": Array [
|
||||
"info",
|
||||
"http",
|
||||
],
|
||||
"type": "log",
|
||||
},
|
||||
Object {
|
||||
"@timestamp": "## @timestamp ##",
|
||||
"message": "starting http server [localhost:8274]",
|
||||
"pid": "## PID ##",
|
||||
"tags": Array [
|
||||
"info",
|
||||
"http",
|
||||
"server",
|
||||
],
|
||||
"type": "log",
|
||||
},
|
||||
Object {
|
||||
"@timestamp": "## @timestamp ##",
|
||||
"message": "Server running at http://localhost:8274",
|
||||
|
|
|
@ -24,8 +24,10 @@ import { resolve } from 'path';
|
|||
|
||||
import { fromRoot } from '../../utils';
|
||||
import { getConfig } from '../../server/path';
|
||||
import { Config } from '../../server/config/config';
|
||||
import { readYamlConfig } from './read_yaml_config';
|
||||
import { readKeystore } from './read_keystore';
|
||||
import { transformDeprecations } from '../../server/config/transform_deprecations';
|
||||
|
||||
import { DEV_SSL_CERT_PATH, DEV_SSL_KEY_PATH } from '../dev_ssl';
|
||||
|
||||
|
@ -236,10 +238,17 @@ export default function (program) {
|
|||
}
|
||||
|
||||
process.on('SIGHUP', async function reloadConfig() {
|
||||
const settings = getCurrentSettings();
|
||||
const settings = transformDeprecations(getCurrentSettings());
|
||||
const config = new Config(kbnServer.config.getSchema(), settings);
|
||||
|
||||
kbnServer.server.log(['info', 'config'], 'Reloading logging configuration due to SIGHUP.');
|
||||
await kbnServer.applyLoggingConfiguration(settings);
|
||||
await kbnServer.applyLoggingConfiguration(config);
|
||||
kbnServer.server.log(['info', 'config'], 'Reloaded logging configuration due to SIGHUP.');
|
||||
|
||||
// If new platform config subscription is active, let's notify it with the updated config.
|
||||
if (kbnServer.newPlatform) {
|
||||
kbnServer.newPlatform.updateConfig(config);
|
||||
}
|
||||
});
|
||||
|
||||
return kbnServer;
|
||||
|
|
18
src/core/README.md
Normal file
18
src/core/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# New Platform (Core)
|
||||
|
||||
All Kibana requests will hit the new platform first and it will decide whether request can be
|
||||
solely handled by the new platform or request should be forwarded to the legacy platform. In this mode new platform does
|
||||
not read config file directly, but rather transforms config provided by the legacy platform. In addition to that all log
|
||||
records are forwarded to the legacy platform so that it can layout and output them properly.
|
||||
|
||||
## Starting plugins in the new platform
|
||||
|
||||
Plugins in `../core_plugins` will be started automatically. In addition, dirs to
|
||||
scan for plugins can be specified in the Kibana config by setting the
|
||||
`__newPlatform.plugins.scanDirs` value, e.g.
|
||||
|
||||
```yaml
|
||||
__newPlatform:
|
||||
plugins:
|
||||
scanDirs: ['./example_plugins']
|
||||
```
|
20
src/core/index.ts
Normal file
20
src/core/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { injectIntoKbnServer, createBasePathProxy } from './server/legacy_compat';
|
44
src/core/lib/kbn_internal_native_observable/README.md
Normal file
44
src/core/lib/kbn_internal_native_observable/README.md
Normal file
|
@ -0,0 +1,44 @@
|
|||
# kbn-internal-native-observable
|
||||
|
||||
This package contains a [spec-compliant][spec] observable implementation that
|
||||
does _not_ implement any additional helper methods on the observable.
|
||||
|
||||
NB! It is not intended to be used directly. It is exposed through
|
||||
`../kbn-observable`, which also exposes several helpers, similar to a subset of
|
||||
features in RxJS.
|
||||
|
||||
## Background
|
||||
|
||||
We only want to expose native JavaScript observables in the api, i.e. exposed
|
||||
observables should _only_ implement the specific methods defined in the spec.
|
||||
The primary reason for doing this is that we don't want to couple our plugin
|
||||
api to a specific version of RxJS (or any other observable library that
|
||||
implements additional methods on top of the spec).
|
||||
|
||||
As there exists no other library we can use in the interim while waiting for the
|
||||
Observable spec to reach stage 3, all exposed observables in the Kibana platform
|
||||
should rely on this package.
|
||||
|
||||
## Why a separate package?
|
||||
|
||||
This package is implemented as a separate package instead of directly in the
|
||||
platform code base for a couple of reasons. We wanted to copy the
|
||||
implementation from the [observable proposal][spec] directly (so it's easier to
|
||||
stay up-to-date with the future spec), and we therefore didn't want to start
|
||||
adding TS types directly to that implementation.
|
||||
|
||||
We tried to avoid this by implementing the type declaration file separately and
|
||||
make that part of the build. However, to handle the JS file we would have to
|
||||
enable the `allowJs` TypeScript compiler option, which doesn't yet play nicely
|
||||
with the automatic building of declaration files we do in the `kbn-types`
|
||||
package.
|
||||
|
||||
The best solution we found in the end was to extract this as a separate package
|
||||
and specify the `types` field in the `package.json`. Then everything works out
|
||||
of the box.
|
||||
|
||||
There is no other reasons for this to be a separate package, so if we find a
|
||||
solution to the above we should consider inlining this implementation into the
|
||||
platform.
|
||||
|
||||
[spec]: https://github.com/tc39/proposal-observable
|
139
src/core/lib/kbn_internal_native_observable/index.d.ts
vendored
Normal file
139
src/core/lib/kbn_internal_native_observable/index.d.ts
vendored
Normal file
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
/* tslint:disable */
|
||||
// This adds a symbol type for `Symbol.observable`, which doesn't exist globally
|
||||
// in TypeScript yet.
|
||||
declare global {
|
||||
export interface SymbolConstructor {
|
||||
readonly observable: symbol;
|
||||
}
|
||||
}
|
||||
|
||||
// These types are based on the Observable proposal readme, see
|
||||
// https://github.com/tc39/proposal-observable#api, with the addition of using
|
||||
// generics to define the type of the `value`.
|
||||
|
||||
declare namespace Observable {
|
||||
interface Subscription {
|
||||
// Cancels the subscription
|
||||
unsubscribe(): void;
|
||||
|
||||
// A boolean value indicating whether the subscription is closed
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
interface Subscribable<T> {
|
||||
subscribe(
|
||||
observerOrNext?: SubscriptionObserver<T> | ((value: T) => void),
|
||||
error?: (error: any) => void,
|
||||
complete?: () => void
|
||||
): Subscription;
|
||||
}
|
||||
|
||||
type ObservableInput<T> = Subscribable<T> | Iterable<T>;
|
||||
|
||||
interface SubscriptionObserver<T> {
|
||||
// Sends the next value in the sequence
|
||||
next(value: T): void;
|
||||
|
||||
// Sends the sequence error
|
||||
error(errorValue: Error): void;
|
||||
|
||||
// Sends the completion notification
|
||||
complete(): void;
|
||||
|
||||
// A boolean value indicating whether the subscription is closed
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
export interface StartObserver<T> {
|
||||
start(subscription: Subscription): void;
|
||||
next?(value: T): void;
|
||||
error?(err: any): void;
|
||||
complete?(): void;
|
||||
}
|
||||
|
||||
export interface NextObserver<T> {
|
||||
start?(subscription: Subscription): void;
|
||||
next(value: T): void;
|
||||
error?(err: any): void;
|
||||
complete?(): void;
|
||||
}
|
||||
|
||||
interface ErrorObserver<T> {
|
||||
start?(subscription: Subscription): void;
|
||||
next?(value: T): void;
|
||||
error(err: any): void;
|
||||
complete?(): void;
|
||||
}
|
||||
|
||||
interface CompletionObserver<T> {
|
||||
start?(subscription: Subscription): void;
|
||||
next?(value: T): void;
|
||||
error?(err: any): void;
|
||||
complete(): void;
|
||||
}
|
||||
|
||||
type PartialObserver<T> =
|
||||
| StartObserver<T>
|
||||
| NextObserver<T>
|
||||
| ErrorObserver<T>
|
||||
| CompletionObserver<T>;
|
||||
|
||||
interface Observer<T> {
|
||||
// Receives the subscription object when `subscribe` is called
|
||||
start(subscription: Subscription): void;
|
||||
|
||||
// Receives the next value in the sequence
|
||||
next(value: T): void;
|
||||
|
||||
// Receives the sequence error
|
||||
error(errorValue: Error): void;
|
||||
|
||||
// Receives a completion notification
|
||||
complete(): void;
|
||||
}
|
||||
|
||||
type SubscriberFunction<T> = (
|
||||
observer: SubscriptionObserver<T>
|
||||
) => void | null | undefined | (() => void) | Subscription;
|
||||
|
||||
class Observable<T> {
|
||||
constructor(subscriber: SubscriberFunction<T>);
|
||||
|
||||
// Subscribes to the sequence with an observer
|
||||
subscribe(): Subscription;
|
||||
subscribe(observer: PartialObserver<T>): Subscription;
|
||||
|
||||
// Subscribes to the sequence with callbacks
|
||||
subscribe(
|
||||
onNext: (val: T) => void,
|
||||
onError?: (err: Error) => void,
|
||||
onComplete?: () => void
|
||||
): Subscription;
|
||||
|
||||
// Returns itself
|
||||
[Symbol.observable](): Observable<T>;
|
||||
|
||||
static of<T>(...items: T[]): Observable<T>;
|
||||
static from<T>(x: ObservableInput<T>): Observable<T>;
|
||||
}
|
||||
}
|
||||
|
||||
export = Observable;
|
322
src/core/lib/kbn_internal_native_observable/index.js
Normal file
322
src/core/lib/kbn_internal_native_observable/index.js
Normal file
|
@ -0,0 +1,322 @@
|
|||
import symbolObservable from 'symbol-observable';
|
||||
|
||||
// This is a fork of the example implementation of the TC39 Observable spec,
|
||||
// see https://github.com/tc39/proposal-observable.
|
||||
//
|
||||
// One change has been applied to work with current libraries: using the
|
||||
// Symbol.observable ponyfill instead of relying on the implementation in the
|
||||
// spec.
|
||||
|
||||
// === Abstract Operations ===
|
||||
|
||||
function nonEnum(obj) {
|
||||
|
||||
Object.getOwnPropertyNames(obj).forEach(k => {
|
||||
Object.defineProperty(obj, k, { enumerable: false });
|
||||
});
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
function getMethod(obj, key) {
|
||||
|
||||
let value = obj[key];
|
||||
|
||||
if (value == null)
|
||||
return undefined;
|
||||
|
||||
if (typeof value !== "function")
|
||||
throw new TypeError(value + " is not a function");
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function cleanupSubscription(subscription) {
|
||||
|
||||
// Assert: observer._observer is undefined
|
||||
|
||||
let cleanup = subscription._cleanup;
|
||||
|
||||
if (!cleanup)
|
||||
return;
|
||||
|
||||
// Drop the reference to the cleanup function so that we won't call it
|
||||
// more than once
|
||||
subscription._cleanup = undefined;
|
||||
|
||||
// Call the cleanup function
|
||||
try {
|
||||
cleanup();
|
||||
}
|
||||
catch(e) {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
}
|
||||
|
||||
function subscriptionClosed(subscription) {
|
||||
|
||||
return subscription._observer === undefined;
|
||||
}
|
||||
|
||||
function closeSubscription(subscription) {
|
||||
|
||||
if (subscriptionClosed(subscription))
|
||||
return;
|
||||
|
||||
subscription._observer = undefined;
|
||||
cleanupSubscription(subscription);
|
||||
}
|
||||
|
||||
function cleanupFromSubscription(subscription) {
|
||||
return _=> { subscription.unsubscribe() };
|
||||
}
|
||||
|
||||
function Subscription(observer, subscriber) {
|
||||
// Assert: subscriber is callable
|
||||
// The observer must be an object
|
||||
this._cleanup = undefined;
|
||||
this._observer = observer;
|
||||
|
||||
// If the observer has a start method, call it with the subscription object
|
||||
try {
|
||||
let start = getMethod(observer, "start");
|
||||
|
||||
if (start) {
|
||||
start.call(observer, this);
|
||||
}
|
||||
}
|
||||
catch(e) {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
|
||||
// If the observer has unsubscribed from the start method, exit
|
||||
if (subscriptionClosed(this))
|
||||
return;
|
||||
|
||||
observer = new SubscriptionObserver(this);
|
||||
|
||||
try {
|
||||
|
||||
// Call the subscriber function
|
||||
let cleanup = subscriber.call(undefined, observer);
|
||||
|
||||
// The return value must be undefined, null, a subscription object, or a function
|
||||
if (cleanup != null) {
|
||||
if (typeof cleanup.unsubscribe === "function")
|
||||
cleanup = cleanupFromSubscription(cleanup);
|
||||
else if (typeof cleanup !== "function")
|
||||
throw new TypeError(cleanup + " is not a function");
|
||||
|
||||
this._cleanup = cleanup;
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
|
||||
// If an error occurs during startup, then send the error
|
||||
// to the observer.
|
||||
observer.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the stream is already finished, then perform cleanup
|
||||
if (subscriptionClosed(this)) {
|
||||
cleanupSubscription(this);
|
||||
}
|
||||
}
|
||||
|
||||
Subscription.prototype = nonEnum({
|
||||
get closed() { return subscriptionClosed(this) },
|
||||
unsubscribe() { closeSubscription(this) },
|
||||
});
|
||||
|
||||
function SubscriptionObserver(subscription) {
|
||||
this._subscription = subscription;
|
||||
}
|
||||
|
||||
SubscriptionObserver.prototype = nonEnum({
|
||||
|
||||
get closed() {
|
||||
|
||||
return subscriptionClosed(this._subscription);
|
||||
},
|
||||
|
||||
next(value) {
|
||||
|
||||
let subscription = this._subscription;
|
||||
|
||||
// If the stream if closed, then return undefined
|
||||
if (subscriptionClosed(subscription))
|
||||
return undefined;
|
||||
|
||||
let observer = subscription._observer;
|
||||
|
||||
try {
|
||||
let m = getMethod(observer, "next");
|
||||
|
||||
// If the observer doesn't support "next", then return undefined
|
||||
if (!m)
|
||||
return undefined;
|
||||
|
||||
// Send the next value to the sink
|
||||
m.call(observer, value);
|
||||
}
|
||||
catch(e) {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
error(value) {
|
||||
|
||||
let subscription = this._subscription;
|
||||
|
||||
// If the stream is closed, throw the error to the caller
|
||||
if (subscriptionClosed(subscription)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let observer = subscription._observer;
|
||||
subscription._observer = undefined;
|
||||
|
||||
try {
|
||||
|
||||
let m = getMethod(observer, "error");
|
||||
|
||||
// If the sink does not support "complete", then return undefined
|
||||
if (m) {
|
||||
m.call(observer, value);
|
||||
}
|
||||
else {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
} catch (e) {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
|
||||
cleanupSubscription(subscription);
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
complete() {
|
||||
|
||||
let subscription = this._subscription;
|
||||
|
||||
// If the stream is closed, then return undefined
|
||||
if (subscriptionClosed(subscription))
|
||||
return undefined;
|
||||
|
||||
let observer = subscription._observer;
|
||||
subscription._observer = undefined;
|
||||
|
||||
try {
|
||||
|
||||
let m = getMethod(observer, "complete");
|
||||
|
||||
// If the sink does not support "complete", then return undefined
|
||||
if (m) {
|
||||
m.call(observer);
|
||||
}
|
||||
} catch (e) {
|
||||
// HostReportErrors(e);
|
||||
}
|
||||
|
||||
cleanupSubscription(subscription);
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export class Observable {
|
||||
|
||||
// == Fundamental ==
|
||||
|
||||
constructor(subscriber) {
|
||||
|
||||
// The stream subscriber must be a function
|
||||
if (typeof subscriber !== "function")
|
||||
throw new TypeError("Observable initializer must be a function");
|
||||
|
||||
this._subscriber = subscriber;
|
||||
}
|
||||
|
||||
subscribe(observer, ...args) {
|
||||
if (typeof observer === "function") {
|
||||
observer = {
|
||||
next: observer,
|
||||
error: args[0],
|
||||
complete: args[1]
|
||||
};
|
||||
}
|
||||
else if (typeof observer !== "object") {
|
||||
observer = {};
|
||||
}
|
||||
|
||||
return new Subscription(observer, this._subscriber);
|
||||
}
|
||||
|
||||
[symbolObservable]() { return this }
|
||||
|
||||
// == Derived ==
|
||||
|
||||
static from(x) {
|
||||
|
||||
let C = typeof this === "function" ? this : Observable;
|
||||
|
||||
if (x == null)
|
||||
throw new TypeError(x + " is not an object");
|
||||
|
||||
let method = getMethod(x, symbolObservable);
|
||||
|
||||
if (method) {
|
||||
|
||||
let observable = method.call(x);
|
||||
|
||||
if (Object(observable) !== observable)
|
||||
throw new TypeError(observable + " is not an object");
|
||||
|
||||
if (observable.constructor === C)
|
||||
return observable;
|
||||
|
||||
return new C(observer => observable.subscribe(observer));
|
||||
}
|
||||
|
||||
method = getMethod(x, Symbol.iterator);
|
||||
|
||||
if (!method)
|
||||
throw new TypeError(x + " is not observable");
|
||||
|
||||
return new C(observer => {
|
||||
|
||||
for (let item of method.call(x)) {
|
||||
|
||||
observer.next(item);
|
||||
|
||||
if (observer.closed)
|
||||
return;
|
||||
}
|
||||
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
static of(...items) {
|
||||
|
||||
let C = typeof this === "function" ? this : Observable;
|
||||
|
||||
return new C(observer => {
|
||||
|
||||
for (let i = 0; i < items.length; ++i) {
|
||||
|
||||
observer.next(items[i]);
|
||||
|
||||
if (observer.closed)
|
||||
return;
|
||||
}
|
||||
|
||||
observer.complete();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
138
src/core/lib/kbn_observable/README.md
Normal file
138
src/core/lib/kbn_observable/README.md
Normal file
|
@ -0,0 +1,138 @@
|
|||
# `kbn-observable`
|
||||
|
||||
kbn-observable is an observable library based on the [proposed `Observable`][proposal]
|
||||
feature. In includes several factory functions and operators, that all return
|
||||
"native" observable.
|
||||
|
||||
Why build this? The main reason is that we don't want to tie our plugin apis
|
||||
heavily to a large dependency, but rather expose something that's much closer
|
||||
to "native" observables, and something we have control over ourselves. Also, all
|
||||
other observable libraries have their own base `Observable` class, while we
|
||||
wanted to rely on the proposed functionality.
|
||||
|
||||
In addition, kbn-observable includes `System.observable`, which enables interop
|
||||
between observable libraries, which means plugins can use whatever observable
|
||||
library they want, if they don't want to use `kbn-observable`.
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
import { Observable, k$, map, last } from '../kbn_observable';
|
||||
|
||||
const source = Observable.of(1, 2, 3);
|
||||
|
||||
// When `k$` is called with the source observable it returns a function that
|
||||
// can be called with "operators" that modify the input value and return an
|
||||
// observable that reflects all of the modifications.
|
||||
k$(source)(map(i => 2017 + i), last())
|
||||
.subscribe(console.log) // logs 2020
|
||||
```
|
||||
|
||||
## Just getting started with Observables?
|
||||
|
||||
If you are just getting started with observables, a great place to start is with
|
||||
Andre Staltz' [The introduction to Reactive Programming you've been missing][staltz-intro],
|
||||
which is a great introduction to the ideas and concepts.
|
||||
|
||||
The ideas in `kbn-observable` is heavily based on [RxJS][rxjs], so the
|
||||
[RxJS docs][rxjs-docs] are also a good source of introduction to observables and
|
||||
how they work in this library.
|
||||
|
||||
**NOTE**: Do you know about good articles, videos or other resources that does
|
||||
a great job at explaining observables? Add them here, so it becomes easier for
|
||||
the next person to learn about them!
|
||||
|
||||
## Factories
|
||||
|
||||
Just like the `k$` function, factories take arguments and produce an observable.
|
||||
Different factories are useful for different things, and many behave just like
|
||||
the static functions attached to the `Rx.Observable` class in RxJS.
|
||||
|
||||
See [./src/factories](./src/factories) for more info about each factory.
|
||||
|
||||
## Operators
|
||||
|
||||
Operators are functions that take some arguments and produce an operator
|
||||
function. Operators aren't anything fancy, just a function that takes an
|
||||
observable and returns a new observable with the requested modifications
|
||||
applied.
|
||||
|
||||
Some examples:
|
||||
|
||||
```js
|
||||
map(i => 2017 + i);
|
||||
|
||||
filter(i => i % 2 === 0)
|
||||
|
||||
reduce((acc, val) => {
|
||||
return acc + val;
|
||||
}, 0);
|
||||
```
|
||||
|
||||
Multiple operator functions can be passed to `k$` and will be applied to the
|
||||
input observable before returning the final observable with all modifications
|
||||
applied, e.g. like the example above with `map` and `last`.
|
||||
|
||||
See [./src/operators](./src/operators) for more info about each operator.
|
||||
|
||||
## More advanced topics
|
||||
|
||||
This library contains implementations of both `Observable` and `Subject`. To
|
||||
better understand the difference between them, it's important to understand the
|
||||
difference between hot and cold observables. Ben Lesh's
|
||||
[Hot vs Cold Observables][hot-vs-cold] is a great introduction to this topic.
|
||||
|
||||
**NOTE**: Do you know about good articles, videos or other resources that goes
|
||||
deeper into Observables and related topics? Make sure we get them added to this
|
||||
list!
|
||||
|
||||
## Why `kbn-observable`?
|
||||
|
||||
While exploring how to handle observables in Kibana we went through multiple
|
||||
PoCs. We initially used RxJS directly, but we didn't find a simple way to
|
||||
consistently transform RxJS observables into "native" observables in the plugin
|
||||
apis. This was something we wanted because of our earlier experiences with
|
||||
exposing large libraries in our apis, which causes problems e.g. when we need to
|
||||
perform major upgrades of a lib that has breaking changes, but we can't ship a
|
||||
new major version of Kibana yet, even though this will cause breaking changes
|
||||
in our plugin apis.
|
||||
|
||||
Then we built the initial version of `kbn-observable` based on the Observable
|
||||
spec, and we included the `k$` helper and several operators that worked like
|
||||
this:
|
||||
|
||||
```js
|
||||
import { k$, Observable, map, first } from 'kbn-observable';
|
||||
|
||||
// general structure:
|
||||
const resultObservable = k$(sourceObservable, [...operators]);
|
||||
|
||||
// e.g.
|
||||
const source = Observable.of(1,2,3);
|
||||
const observable = k$(source, [map(x => x + 1), first()]);
|
||||
```
|
||||
|
||||
Here `Observable` is a copy of the Observable class from the spec. This
|
||||
would enable us to always work with these spec-ed observables. This api for `k$`
|
||||
worked nicely in pure JavaScript, but caused a problem with TypeScript, as
|
||||
TypeScript wasn't able to correctly type the operators array when more than one
|
||||
operator was specified.
|
||||
|
||||
Because of that problem we ended up with `k$(source)(...operators)`. With this
|
||||
change TypeScript is able to correctly type the operator arguments.
|
||||
|
||||
We've also discussed adding a `pipe` method to the `Observable.prototype`, so we
|
||||
could do `source.pipe(...operators)` instead, but we decided against it because
|
||||
we didn't want to start adding features directly on the `Observable` object, but
|
||||
rather follow the spec as close as possible, and only update whenever the spec
|
||||
receives updates.
|
||||
|
||||
## Inspiration
|
||||
|
||||
This code is heavily inspired by and based on RxJS, which is licensed under the
|
||||
Apache License, Version 2.0, see https://github.com/ReactiveX/rxjs.
|
||||
|
||||
[proposal]: https://github.com/tc39/proposal-observable
|
||||
[rxjs]: http://reactivex.io/rxjs/
|
||||
[rxjs-docs]: http://reactivex.io/rxjs/manual/index.html
|
||||
[staltz-intro]: https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
|
|
@ -0,0 +1,3 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should throw if it has received an error and getValue() is called 1`] = `"derp"`;
|
186
src/core/lib/kbn_observable/__tests__/behavior_subject.test.ts
Normal file
186
src/core/lib/kbn_observable/__tests__/behavior_subject.test.ts
Normal file
|
@ -0,0 +1,186 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from '../behavior_subject';
|
||||
import { collect } from '../lib/collect';
|
||||
import { Observable } from '../observable';
|
||||
import { Subject } from '../subject';
|
||||
|
||||
test('should extend Subject', () => {
|
||||
const subject = new BehaviorSubject(null);
|
||||
expect(subject).toBeInstanceOf(Subject);
|
||||
});
|
||||
|
||||
test('should throw if it has received an error and getValue() is called', () => {
|
||||
const subject = new BehaviorSubject(null);
|
||||
|
||||
subject.error(new Error('derp'));
|
||||
|
||||
expect(() => {
|
||||
subject.getValue();
|
||||
}).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('should have a getValue() method to retrieve the current value', () => {
|
||||
const subject = new BehaviorSubject('foo');
|
||||
|
||||
expect(subject.getValue()).toEqual('foo');
|
||||
|
||||
subject.next('bar');
|
||||
|
||||
expect(subject.getValue()).toEqual('bar');
|
||||
});
|
||||
|
||||
test('should not update value after completed', () => {
|
||||
const subject = new BehaviorSubject('foo');
|
||||
|
||||
expect(subject.getValue()).toEqual('foo');
|
||||
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
subject.next('quux');
|
||||
|
||||
expect(subject.getValue()).toEqual('bar');
|
||||
});
|
||||
|
||||
test('should start with an initialization value', async () => {
|
||||
const subject = new BehaviorSubject('foo');
|
||||
const res = collect(subject);
|
||||
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foo', 'bar', 'C']);
|
||||
});
|
||||
|
||||
test('should pump values to multiple subscribers', async () => {
|
||||
const subject = new BehaviorSubject('init');
|
||||
const expected = ['init', 'foo', 'bar', 'C'];
|
||||
|
||||
const res1 = collect(subject);
|
||||
const res2 = collect(subject);
|
||||
|
||||
expect((subject as any).observers.size).toEqual(2);
|
||||
subject.next('foo');
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
|
||||
expect(await res1).toEqual(expected);
|
||||
expect(await res2).toEqual(expected);
|
||||
});
|
||||
|
||||
test('should not pass values nexted after a complete', () => {
|
||||
const subject = new BehaviorSubject('init');
|
||||
const results: any[] = [];
|
||||
|
||||
subject.subscribe(x => {
|
||||
results.push(x);
|
||||
});
|
||||
expect(results).toEqual(['init']);
|
||||
|
||||
subject.next('foo');
|
||||
expect(results).toEqual(['init', 'foo']);
|
||||
|
||||
subject.complete();
|
||||
expect(results).toEqual(['init', 'foo']);
|
||||
|
||||
subject.next('bar');
|
||||
expect(results).toEqual(['init', 'foo']);
|
||||
});
|
||||
|
||||
test('should clean out unsubscribed subscribers', () => {
|
||||
const subject = new BehaviorSubject('init');
|
||||
|
||||
const sub1 = subject.subscribe();
|
||||
const sub2 = subject.subscribe();
|
||||
|
||||
expect((subject as any).observers.size).toEqual(2);
|
||||
|
||||
sub1.unsubscribe();
|
||||
expect((subject as any).observers.size).toEqual(1);
|
||||
|
||||
sub2.unsubscribe();
|
||||
expect((subject as any).observers.size).toEqual(0);
|
||||
});
|
||||
|
||||
test('should replay the previous value when subscribed', () => {
|
||||
const subject = new BehaviorSubject(0);
|
||||
|
||||
subject.next(1);
|
||||
subject.next(2);
|
||||
|
||||
const s1Actual: number[] = [];
|
||||
const s1 = subject.subscribe(x => {
|
||||
s1Actual.push(x);
|
||||
});
|
||||
|
||||
subject.next(3);
|
||||
subject.next(4);
|
||||
|
||||
const s2Actual: number[] = [];
|
||||
const s2 = subject.subscribe(x => {
|
||||
s2Actual.push(x);
|
||||
});
|
||||
|
||||
s1.unsubscribe();
|
||||
|
||||
subject.next(5);
|
||||
|
||||
const s3Actual: number[] = [];
|
||||
const s3 = subject.subscribe(x => {
|
||||
s3Actual.push(x);
|
||||
});
|
||||
|
||||
s2.unsubscribe();
|
||||
s3.unsubscribe();
|
||||
|
||||
subject.complete();
|
||||
|
||||
expect(s1Actual).toEqual([2, 3, 4]);
|
||||
expect(s2Actual).toEqual([4, 5]);
|
||||
expect(s3Actual).toEqual([5]);
|
||||
});
|
||||
|
||||
test('should emit complete when subscribed after completed', () => {
|
||||
const source = Observable.of(1, 2, 3, 4, 5);
|
||||
const subject = new BehaviorSubject(0);
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
|
||||
subject.complete();
|
||||
|
||||
subject.subscribe(next, undefined, complete);
|
||||
source.subscribe(subject);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should be an Observer which can be given to Observable.subscribe', async () => {
|
||||
const source = Observable.of(1, 2, 3, 4, 5);
|
||||
const subject = new BehaviorSubject(0);
|
||||
|
||||
const res = collect(subject);
|
||||
|
||||
source.subscribe(subject);
|
||||
|
||||
expect(await res).toEqual([0, 1, 2, 3, 4, 5, 'C']);
|
||||
expect(subject.getValue()).toBe(5);
|
||||
});
|
90
src/core/lib/kbn_observable/__tests__/k.test.ts
Normal file
90
src/core/lib/kbn_observable/__tests__/k.test.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { MonoTypeOperatorFunction, OperatorFunction, UnaryFunction } from '../interfaces';
|
||||
import { k$ } from '../k';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
const plus1: MonoTypeOperatorFunction<number> = source =>
|
||||
new Observable(observer => {
|
||||
source.subscribe({
|
||||
next(val) {
|
||||
observer.next(val + 1);
|
||||
},
|
||||
error(err) {
|
||||
observer.error(err);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const toString: OperatorFunction<number, string> = source =>
|
||||
new Observable(observer => {
|
||||
source.subscribe({
|
||||
next(val) {
|
||||
observer.next(val.toString());
|
||||
},
|
||||
error(err) {
|
||||
observer.error(err);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const toPromise: UnaryFunction<Observable<number>, Promise<number>> = source =>
|
||||
new Promise((resolve, reject) => {
|
||||
let lastValue: number;
|
||||
|
||||
source.subscribe({
|
||||
next(value) {
|
||||
lastValue = value;
|
||||
},
|
||||
error(error) {
|
||||
reject(error);
|
||||
},
|
||||
complete() {
|
||||
resolve(lastValue);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('observable to observable', () => {
|
||||
const numbers$ = Observable.of(1, 2, 3);
|
||||
const actual: any[] = [];
|
||||
|
||||
k$(numbers$)(plus1, toString).subscribe({
|
||||
next(x) {
|
||||
actual.push(x);
|
||||
},
|
||||
});
|
||||
|
||||
expect(actual).toEqual(['2', '3', '4']);
|
||||
});
|
||||
|
||||
test('observable to promise', async () => {
|
||||
const numbers$ = Observable.of(1, 2, 3);
|
||||
|
||||
const value = await k$(numbers$)(plus1, toPromise);
|
||||
|
||||
expect(value).toEqual(4);
|
||||
});
|
160
src/core/lib/kbn_observable/__tests__/observable.test.ts
Normal file
160
src/core/lib/kbn_observable/__tests__/observable.test.ts
Normal file
|
@ -0,0 +1,160 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable, SubscriptionObserver } from '../observable';
|
||||
|
||||
test('receives values when subscribed', async () => {
|
||||
let observer: SubscriptionObserver<any>;
|
||||
|
||||
const source = new Observable(innerObservable => {
|
||||
observer = innerObservable;
|
||||
});
|
||||
|
||||
const res: any[] = [];
|
||||
|
||||
source.subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
});
|
||||
|
||||
observer!.next('foo');
|
||||
expect(res).toEqual(['foo']);
|
||||
|
||||
observer!.next('bar');
|
||||
expect(res).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('triggers complete when observer is completed', async () => {
|
||||
let observer: SubscriptionObserver<any>;
|
||||
|
||||
const source = new Observable(innerObservable => {
|
||||
observer = innerObservable;
|
||||
});
|
||||
|
||||
const complete = jest.fn();
|
||||
|
||||
source.subscribe({
|
||||
complete,
|
||||
});
|
||||
|
||||
observer!.complete();
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should send errors thrown in the constructor down the error path', async () => {
|
||||
const err = new Error('this should be handled');
|
||||
|
||||
const source = new Observable(observer => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const error = jest.fn();
|
||||
|
||||
source.subscribe({
|
||||
error,
|
||||
});
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error).toHaveBeenCalledWith(err);
|
||||
});
|
||||
|
||||
describe('subscriptions', () => {
|
||||
test('handles multiple subscriptions and unsubscriptions', () => {
|
||||
let observers = 0;
|
||||
|
||||
const source = new Observable(observer => {
|
||||
observers++;
|
||||
|
||||
return () => {
|
||||
observers--;
|
||||
};
|
||||
});
|
||||
|
||||
const sub1 = source.subscribe();
|
||||
expect(observers).toBe(1);
|
||||
|
||||
const sub2 = source.subscribe();
|
||||
expect(observers).toBe(2);
|
||||
|
||||
sub1.unsubscribe();
|
||||
expect(observers).toBe(1);
|
||||
|
||||
sub2.unsubscribe();
|
||||
expect(observers).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Observable.from', () => {
|
||||
test('handles array', () => {
|
||||
const res: number[] = [];
|
||||
const complete = jest.fn();
|
||||
|
||||
Observable.from([1, 2, 3]).subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
complete,
|
||||
});
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(res).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('handles iterable', () => {
|
||||
const fooIterable: Iterable<number> = {
|
||||
*[Symbol.iterator]() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
},
|
||||
};
|
||||
|
||||
const res: number[] = [];
|
||||
const complete = jest.fn();
|
||||
|
||||
Observable.from(fooIterable).subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
complete,
|
||||
});
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(res).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Observable.of', () => {
|
||||
test('handles multiple args', () => {
|
||||
const res: number[] = [];
|
||||
const complete = jest.fn();
|
||||
|
||||
Observable.of(1, 2, 3).subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
complete,
|
||||
});
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(res).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
508
src/core/lib/kbn_observable/__tests__/subject.test.ts
Normal file
508
src/core/lib/kbn_observable/__tests__/subject.test.ts
Normal file
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { k$ } from '../k';
|
||||
import { Observable } from '../observable';
|
||||
import { first } from '../operators';
|
||||
import { Subject } from '../subject';
|
||||
|
||||
const noop = () => {
|
||||
// noop
|
||||
};
|
||||
|
||||
test('should pump values right on through itself', () => {
|
||||
const subject = new Subject<string>();
|
||||
const actual: string[] = [];
|
||||
|
||||
subject.subscribe(x => {
|
||||
actual.push(x);
|
||||
});
|
||||
|
||||
subject.next('foo');
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
|
||||
expect(actual).toEqual(['foo', 'bar']);
|
||||
});
|
||||
|
||||
test('should pump values to multiple subscribers', () => {
|
||||
const subject = new Subject<string>();
|
||||
const actual: string[] = [];
|
||||
|
||||
subject.subscribe(x => {
|
||||
actual.push(`1-${x}`);
|
||||
});
|
||||
|
||||
subject.subscribe(x => {
|
||||
actual.push(`2-${x}`);
|
||||
});
|
||||
|
||||
expect((subject as any).observers.size).toEqual(2);
|
||||
subject.next('foo');
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
|
||||
expect(actual).toEqual(['1-foo', '2-foo', '1-bar', '2-bar']);
|
||||
});
|
||||
|
||||
test('should handle subscribers that arrive and leave at different times, subject does not complete', () => {
|
||||
const subject = new Subject();
|
||||
const results1: any[] = [];
|
||||
const results2: any[] = [];
|
||||
const results3: any[] = [];
|
||||
|
||||
subject.next(1);
|
||||
subject.next(2);
|
||||
subject.next(3);
|
||||
subject.next(4);
|
||||
|
||||
const subscription1 = subject.subscribe(
|
||||
x => {
|
||||
results1.push(x);
|
||||
},
|
||||
e => {
|
||||
results1.push('E');
|
||||
},
|
||||
() => {
|
||||
results1.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(5);
|
||||
|
||||
const subscription2 = subject.subscribe(
|
||||
x => {
|
||||
results2.push(x);
|
||||
},
|
||||
e => {
|
||||
results2.push('E');
|
||||
},
|
||||
() => {
|
||||
results2.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(6);
|
||||
subject.next(7);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
|
||||
subject.next(8);
|
||||
|
||||
subscription2.unsubscribe();
|
||||
|
||||
subject.next(9);
|
||||
subject.next(10);
|
||||
|
||||
const subscription3 = subject.subscribe(
|
||||
x => {
|
||||
results3.push(x);
|
||||
},
|
||||
e => {
|
||||
results3.push('E');
|
||||
},
|
||||
() => {
|
||||
results3.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(11);
|
||||
|
||||
subscription3.unsubscribe();
|
||||
|
||||
expect(results1).toEqual([5, 6, 7]);
|
||||
expect(results2).toEqual([6, 7, 8]);
|
||||
expect(results3).toEqual([11]);
|
||||
});
|
||||
|
||||
test('should handle subscribers that arrive and leave at different times, subject completes', () => {
|
||||
const subject = new Subject();
|
||||
const results1: any[] = [];
|
||||
const results2: any[] = [];
|
||||
const results3: any[] = [];
|
||||
|
||||
subject.next(1);
|
||||
subject.next(2);
|
||||
subject.next(3);
|
||||
subject.next(4);
|
||||
|
||||
const subscription1 = subject.subscribe(
|
||||
x => {
|
||||
results1.push(x);
|
||||
},
|
||||
e => {
|
||||
results1.push('E');
|
||||
},
|
||||
() => {
|
||||
results1.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(5);
|
||||
|
||||
const subscription2 = subject.subscribe(
|
||||
x => {
|
||||
results2.push(x);
|
||||
},
|
||||
e => {
|
||||
results2.push('E');
|
||||
},
|
||||
() => {
|
||||
results2.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(6);
|
||||
subject.next(7);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
|
||||
subject.complete();
|
||||
|
||||
subscription2.unsubscribe();
|
||||
|
||||
const subscription3 = subject.subscribe(
|
||||
x => {
|
||||
results3.push(x);
|
||||
},
|
||||
e => {
|
||||
results3.push('E');
|
||||
},
|
||||
() => {
|
||||
results3.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subscription3.unsubscribe();
|
||||
|
||||
expect(results1).toEqual([5, 6, 7]);
|
||||
expect(results2).toEqual([6, 7, 'C']);
|
||||
expect(results3).toEqual(['C']);
|
||||
});
|
||||
|
||||
test('should handle subscribers that arrive and leave at different times, subject terminates with an error', () => {
|
||||
const subject = new Subject();
|
||||
const results1: any[] = [];
|
||||
const results2: any[] = [];
|
||||
const results3: any[] = [];
|
||||
|
||||
subject.next(1);
|
||||
subject.next(2);
|
||||
subject.next(3);
|
||||
subject.next(4);
|
||||
|
||||
const subscription1 = subject.subscribe(
|
||||
x => {
|
||||
results1.push(x);
|
||||
},
|
||||
e => {
|
||||
results1.push('E');
|
||||
},
|
||||
() => {
|
||||
results1.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(5);
|
||||
|
||||
const subscription2 = subject.subscribe(
|
||||
x => {
|
||||
results2.push(x);
|
||||
},
|
||||
e => {
|
||||
results2.push('E');
|
||||
},
|
||||
() => {
|
||||
results2.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subject.next(6);
|
||||
subject.next(7);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
|
||||
subject.error(new Error('err'));
|
||||
|
||||
subscription2.unsubscribe();
|
||||
|
||||
const subscription3 = subject.subscribe(
|
||||
x => {
|
||||
results3.push(x);
|
||||
},
|
||||
e => {
|
||||
results3.push('E');
|
||||
},
|
||||
() => {
|
||||
results3.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subscription3.unsubscribe();
|
||||
|
||||
expect(results1).toEqual([5, 6, 7]);
|
||||
expect(results2).toEqual([6, 7, 'E']);
|
||||
expect(results3).toEqual(['E']);
|
||||
});
|
||||
|
||||
test('should handle subscribers that arrive and leave at different times, subject completes before nexting any value', () => {
|
||||
const subject = new Subject();
|
||||
const results1: any[] = [];
|
||||
const results2: any[] = [];
|
||||
const results3: any[] = [];
|
||||
|
||||
const subscription1 = subject.subscribe(
|
||||
x => {
|
||||
results1.push(x);
|
||||
},
|
||||
e => {
|
||||
results1.push('E');
|
||||
},
|
||||
() => {
|
||||
results1.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
const subscription2 = subject.subscribe(
|
||||
x => {
|
||||
results2.push(x);
|
||||
},
|
||||
e => {
|
||||
results2.push('E');
|
||||
},
|
||||
() => {
|
||||
results2.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subscription1.unsubscribe();
|
||||
|
||||
subject.complete();
|
||||
|
||||
subscription2.unsubscribe();
|
||||
|
||||
const subscription3 = subject.subscribe(
|
||||
x => {
|
||||
results3.push(x);
|
||||
},
|
||||
e => {
|
||||
results3.push('E');
|
||||
},
|
||||
() => {
|
||||
results3.push('C');
|
||||
}
|
||||
);
|
||||
|
||||
subscription3.unsubscribe();
|
||||
|
||||
expect(results1).toEqual([]);
|
||||
expect(results2).toEqual(['C']);
|
||||
expect(results3).toEqual(['C']);
|
||||
});
|
||||
|
||||
test('should clean out unsubscribed subscribers', () => {
|
||||
const subject = new Subject();
|
||||
|
||||
const sub1 = subject.subscribe(noop);
|
||||
const sub2 = subject.subscribe(noop);
|
||||
|
||||
expect((subject as any).observers.size).toBe(2);
|
||||
|
||||
sub1.unsubscribe();
|
||||
expect((subject as any).observers.size).toBe(1);
|
||||
|
||||
sub2.unsubscribe();
|
||||
expect((subject as any).observers.size).toBe(0);
|
||||
});
|
||||
|
||||
test('should be an Observer which can be given to Observable.subscribe', () => {
|
||||
const source = Observable.of(1, 2, 3, 4, 5);
|
||||
const subject = new Subject<number>();
|
||||
const actual: number[] = [];
|
||||
|
||||
const err = jest.fn();
|
||||
const complete = jest.fn();
|
||||
|
||||
subject.subscribe(
|
||||
x => {
|
||||
actual.push(x);
|
||||
},
|
||||
err,
|
||||
complete
|
||||
);
|
||||
|
||||
source.subscribe(subject);
|
||||
|
||||
expect(actual).toEqual([1, 2, 3, 4, 5]);
|
||||
expect(err).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('can use subject in $k', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const error = jest.fn();
|
||||
|
||||
k$(values$)(first()).subscribe({
|
||||
complete,
|
||||
error,
|
||||
next,
|
||||
});
|
||||
|
||||
values$.next('test');
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith('test');
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should not next after completed', () => {
|
||||
const subject = new Subject<string>();
|
||||
const results: any[] = [];
|
||||
|
||||
subject.subscribe(x => results.push(x), undefined, () => results.push('C'));
|
||||
|
||||
subject.next('a');
|
||||
subject.complete();
|
||||
subject.next('b');
|
||||
|
||||
expect(results).toEqual(['a', 'C']);
|
||||
});
|
||||
|
||||
test('should not next after error', () => {
|
||||
const error = new Error('wut?');
|
||||
const subject = new Subject();
|
||||
const results: any[] = [];
|
||||
|
||||
subject.subscribe(x => results.push(x), err => results.push(err));
|
||||
|
||||
subject.next('a');
|
||||
subject.error(error);
|
||||
subject.next('b');
|
||||
|
||||
expect(results).toEqual(['a', error]);
|
||||
});
|
||||
|
||||
describe('asObservable', () => {
|
||||
test('should hide subject', () => {
|
||||
const subject = new Subject();
|
||||
const observable = subject.asObservable();
|
||||
|
||||
expect(subject).not.toBe(observable);
|
||||
|
||||
expect(observable).toBeInstanceOf(Observable);
|
||||
expect(observable).not.toBeInstanceOf(Subject);
|
||||
});
|
||||
|
||||
test('should handle subject completes without emits', () => {
|
||||
const subject = new Subject();
|
||||
|
||||
const complete = jest.fn();
|
||||
|
||||
subject.asObservable().subscribe({
|
||||
complete,
|
||||
});
|
||||
|
||||
subject.complete();
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should handle subject throws', () => {
|
||||
const subject = new Subject();
|
||||
|
||||
const error = jest.fn();
|
||||
|
||||
subject.asObservable().subscribe({
|
||||
error,
|
||||
});
|
||||
|
||||
const e = new Error('yep');
|
||||
subject.error(e);
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error).toHaveBeenCalledWith(e);
|
||||
});
|
||||
|
||||
test('should handle subject emits', () => {
|
||||
const subject = new Subject<number>();
|
||||
|
||||
const actual: number[] = [];
|
||||
|
||||
subject.asObservable().subscribe({
|
||||
next(x) {
|
||||
actual.push(x);
|
||||
},
|
||||
});
|
||||
|
||||
subject.next(1);
|
||||
subject.next(2);
|
||||
subject.complete();
|
||||
|
||||
expect(actual).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test('can unsubscribe', () => {
|
||||
const subject = new Subject<number>();
|
||||
|
||||
const actual: number[] = [];
|
||||
|
||||
const sub = subject.asObservable().subscribe({
|
||||
next(x) {
|
||||
actual.push(x);
|
||||
},
|
||||
});
|
||||
|
||||
subject.next(1);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
subject.next(2);
|
||||
subject.complete();
|
||||
|
||||
expect(actual).toEqual([1]);
|
||||
});
|
||||
|
||||
test('should handle multiple observables', () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const actual: string[] = [];
|
||||
|
||||
subject.asObservable().subscribe({
|
||||
next(x) {
|
||||
actual.push(`1-${x}`);
|
||||
},
|
||||
});
|
||||
|
||||
subject.asObservable().subscribe({
|
||||
next(x) {
|
||||
actual.push(`2-${x}`);
|
||||
},
|
||||
});
|
||||
|
||||
subject.next('foo');
|
||||
subject.next('bar');
|
||||
subject.complete();
|
||||
|
||||
expect(actual).toEqual(['1-foo', '2-foo', '1-bar', '2-bar']);
|
||||
});
|
||||
});
|
63
src/core/lib/kbn_observable/behavior_subject.ts
Normal file
63
src/core/lib/kbn_observable/behavior_subject.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { SubscriptionObserver } from './observable';
|
||||
import { Subject } from './subject';
|
||||
|
||||
/**
|
||||
* A BehaviorSubject is a Subject that has a _current_ value.
|
||||
*
|
||||
* Whenever an observer subscribes to a BehaviorSubject, it begins by emitting
|
||||
* the item most recently emitted by the source Observable (or a seed/default
|
||||
* value if none has yet been emitted) and then continues to emit any other
|
||||
* items emitted later by the source Observable(s).
|
||||
*/
|
||||
export class BehaviorSubject<T> extends Subject<T> {
|
||||
constructor(private value: T) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The current value of the BehaviorSubject. Most of the time this
|
||||
* shouldn't be used directly, but there are situations were it can come in
|
||||
* handy. Usually a BehaviorSubject is used so you immediately receive the
|
||||
* latest/current value when subscribing.
|
||||
*/
|
||||
public getValue() {
|
||||
if (this.thrownError !== undefined) {
|
||||
throw this.thrownError;
|
||||
}
|
||||
|
||||
return this.value;
|
||||
}
|
||||
|
||||
public next(value: T) {
|
||||
if (!this.isStopped) {
|
||||
this.value = value;
|
||||
}
|
||||
return super.next(value);
|
||||
}
|
||||
|
||||
protected registerObserver(observer: SubscriptionObserver<T>) {
|
||||
if (!this.isStopped) {
|
||||
observer.next(this.value);
|
||||
}
|
||||
return super.registerObserver(observer);
|
||||
}
|
||||
}
|
31
src/core/lib/kbn_observable/errors/empty_error.ts
Normal file
31
src/core/lib/kbn_observable/errors/empty_error.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export class EmptyError extends Error {
|
||||
public code = 'K$_EMPTY_ERROR';
|
||||
|
||||
constructor(producer: string) {
|
||||
super(`EmptyError: ${producer} requires source stream to emit at least one value.`);
|
||||
|
||||
// We're forching this to `any` as `captureStackTrace` is not a standard
|
||||
// property, but a v8 specific one. There are node typings that we might
|
||||
// want to use, see https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/node/index.d.ts#L47
|
||||
(Error as any).captureStackTrace(this, EmptyError);
|
||||
}
|
||||
}
|
20
src/core/lib/kbn_observable/errors/index.ts
Normal file
20
src/core/lib/kbn_observable/errors/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { EmptyError } from './empty_error';
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`errors if callback is called with more than two args 1`] = `
|
||||
Array [
|
||||
[Error: Node callback called with too many args],
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { collect } from '../../lib/collect';
|
||||
import { $bindNodeCallback } from '../bind_node_callback';
|
||||
|
||||
type NodeCallback = (err: any, val?: string) => void;
|
||||
|
||||
test('callback with error', async () => {
|
||||
const error = new Error('fail');
|
||||
const read = (cb: NodeCallback) => cb(error);
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$());
|
||||
|
||||
expect(await res).toEqual([error]);
|
||||
});
|
||||
|
||||
test('callback with value', async () => {
|
||||
const read = (cb: NodeCallback) => cb(undefined, 'test');
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$());
|
||||
|
||||
expect(await res).toEqual(['test', 'C']);
|
||||
});
|
||||
|
||||
test('does not treat `null` as error', async () => {
|
||||
const read = (cb: NodeCallback) => cb(null, 'test');
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$());
|
||||
|
||||
expect(await res).toEqual(['test', 'C']);
|
||||
});
|
||||
|
||||
test('multiple args', async () => {
|
||||
const read = (arg1: string, arg2: number, cb: NodeCallback) => cb(undefined, `${arg1}/${arg2}`);
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$('foo', 123));
|
||||
|
||||
expect(await res).toEqual(['foo/123', 'C']);
|
||||
});
|
||||
|
||||
test('function throws instead of calling callback', async () => {
|
||||
const error = new Error('fail');
|
||||
|
||||
const read = (cb: NodeCallback) => {
|
||||
throw error;
|
||||
};
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$());
|
||||
|
||||
expect(await res).toEqual([error]);
|
||||
});
|
||||
|
||||
test('errors if callback is called with more than two args', async () => {
|
||||
const read = (cb: (...args: any[]) => any) => cb(undefined, 'arg1', 'arg2');
|
||||
|
||||
const read$ = $bindNodeCallback(read);
|
||||
const res = collect(read$());
|
||||
|
||||
expect(await res).toMatchSnapshot();
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $combineLatest, $of } from '../../factories';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
const tickMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
test('emits once for each combination of items', async () => {
|
||||
const foo$ = new Subject();
|
||||
const bar$ = new Subject();
|
||||
|
||||
const observable = $combineLatest(foo$, bar$);
|
||||
const res = collect(observable);
|
||||
|
||||
await tickMs(10);
|
||||
bar$.next('a');
|
||||
|
||||
await tickMs(5);
|
||||
foo$.next(1);
|
||||
|
||||
await tickMs(5);
|
||||
bar$.next('b');
|
||||
|
||||
await tickMs(5);
|
||||
foo$.next(2);
|
||||
bar$.next('c');
|
||||
|
||||
await tickMs(10);
|
||||
foo$.next(3);
|
||||
|
||||
bar$.complete();
|
||||
foo$.complete();
|
||||
|
||||
expect(await res).toEqual([[1, 'a'], [1, 'b'], [2, 'b'], [2, 'c'], [3, 'c'], 'C']);
|
||||
});
|
||||
|
||||
test('only emits if every stream emits at least once', async () => {
|
||||
const empty$ = $of();
|
||||
const three$ = $of(1, 2, 3);
|
||||
|
||||
const observable = $combineLatest(empty$, three$);
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual(['C']);
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $concat } from '../';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('continous on next observable when previous completes', async () => {
|
||||
const a = new Subject();
|
||||
const b = new Subject();
|
||||
|
||||
const observable = $concat(a, b);
|
||||
const res = collect(observable);
|
||||
|
||||
a.next('a1');
|
||||
b.next('b1');
|
||||
a.next('a2');
|
||||
a.complete();
|
||||
b.next('b2');
|
||||
b.complete();
|
||||
|
||||
expect(await res).toEqual(['a1', 'a2', 'b2', 'C']);
|
||||
});
|
||||
|
||||
test('errors when any observable errors', async () => {
|
||||
const a = new Subject();
|
||||
const b = new Subject();
|
||||
|
||||
const observable = $concat(a, b);
|
||||
const res = collect(observable);
|
||||
|
||||
const error = new Error('fail');
|
||||
a.next('a1');
|
||||
a.error(error);
|
||||
|
||||
expect(await res).toEqual(['a1', error]);
|
||||
});
|
||||
|
||||
test('handles early unsubscribe', () => {
|
||||
const a = new Subject();
|
||||
const b = new Subject();
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
const sub = $concat(a, b).subscribe({ next, complete });
|
||||
|
||||
a.next('a1');
|
||||
sub.unsubscribe();
|
||||
a.next('a2');
|
||||
a.complete();
|
||||
b.next('b1');
|
||||
b.complete();
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith('a1');
|
||||
expect(complete).toHaveBeenCalledTimes(0);
|
||||
});
|
58
src/core/lib/kbn_observable/factories/__tests__/from.test.ts
Normal file
58
src/core/lib/kbn_observable/factories/__tests__/from.test.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $from } from '../../factories';
|
||||
|
||||
test('handles array', () => {
|
||||
const res: number[] = [];
|
||||
const complete = jest.fn();
|
||||
|
||||
$from([1, 2, 3]).subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
complete,
|
||||
});
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(res).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
test('handles iterable', () => {
|
||||
const fooIterable: Iterable<number> = {
|
||||
*[Symbol.iterator]() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
yield 3;
|
||||
},
|
||||
};
|
||||
|
||||
const res: number[] = [];
|
||||
const complete = jest.fn();
|
||||
|
||||
$from(fooIterable).subscribe({
|
||||
next(x) {
|
||||
res.push(x);
|
||||
},
|
||||
complete,
|
||||
});
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
expect(res).toEqual([1, 2, 3]);
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $from } from '../';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
import { $fromCallback } from '../from_callback';
|
||||
|
||||
test('returns raw value', async () => {
|
||||
const observable = $fromCallback(() => 'foo');
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual(['foo', 'C']);
|
||||
});
|
||||
|
||||
test('if undefined is returned, completes immediatley', async () => {
|
||||
const observable = $fromCallback(() => undefined);
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual(['C']);
|
||||
});
|
||||
|
||||
test('if null is returned, forwards it', async () => {
|
||||
const observable = $fromCallback(() => null);
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual([null, 'C']);
|
||||
});
|
||||
|
||||
test('returns observable that completes immediately', async () => {
|
||||
const observable = $fromCallback(() => $from([1, 2, 3]));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual([1, 2, 3, 'C']);
|
||||
});
|
||||
|
||||
test('returns observable that completes later', () => {
|
||||
const subject = new Subject();
|
||||
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const complete = jest.fn();
|
||||
|
||||
$fromCallback(() => subject).subscribe(next, error, complete);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
|
||||
subject.next('foo');
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).not.toHaveBeenCalled();
|
||||
|
||||
subject.complete();
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('handles early unsubscribe', () => {
|
||||
const subject = new Subject();
|
||||
|
||||
const next = () => {
|
||||
// noop
|
||||
};
|
||||
const sub = $fromCallback(() => subject).subscribe(next);
|
||||
|
||||
subject.next('foo');
|
||||
|
||||
expect((subject as any).observers.size).toEqual(1);
|
||||
sub.unsubscribe();
|
||||
expect((subject as any).observers.size).toEqual(0);
|
||||
});
|
100
src/core/lib/kbn_observable/factories/bind_node_callback.ts
Normal file
100
src/core/lib/kbn_observable/factories/bind_node_callback.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from '../observable';
|
||||
|
||||
export function $bindNodeCallback<R>(
|
||||
callbackFunc: (callback: (err: any, result: R) => any) => any
|
||||
): () => Observable<R>;
|
||||
export function $bindNodeCallback<T, R>(
|
||||
callbackFunc: (v1: T, callback: (err: any, result: R) => any) => any
|
||||
): (v1: T) => Observable<R>;
|
||||
export function $bindNodeCallback<T, T2, R>(
|
||||
callbackFunc: (v1: T, v2: T2, callback: (err: any, result: R) => any) => any
|
||||
): (v1: T, v2: T2) => Observable<R>;
|
||||
export function $bindNodeCallback<T, T2, T3, R>(
|
||||
callbackFunc: (v1: T, v2: T2, v3: T3, callback: (err: any, result: R) => any) => any
|
||||
): (v1: T, v2: T2, v3: T3) => Observable<R>;
|
||||
export function $bindNodeCallback<T, T2, T3, T4, R>(
|
||||
callbackFunc: (v1: T, v2: T2, v3: T3, v4: T4, callback: (err: any, result: R) => any) => any
|
||||
): (v1: T, v2: T2, v3: T3, v4: T4) => Observable<R>;
|
||||
export function $bindNodeCallback<T, T2, T3, T4, T5, R>(
|
||||
callbackFunc: (
|
||||
v1: T,
|
||||
v2: T2,
|
||||
v3: T3,
|
||||
v4: T4,
|
||||
v5: T5,
|
||||
callback: (err: any, result: R) => any
|
||||
) => any
|
||||
): (v1: T, v2: T2, v3: T3, v4: T4, v5: T5) => Observable<R>;
|
||||
export function $bindNodeCallback<T, T2, T3, T4, T5, T6, R>(
|
||||
callbackFunc: (
|
||||
v1: T,
|
||||
v2: T2,
|
||||
v3: T3,
|
||||
v4: T4,
|
||||
v5: T5,
|
||||
v6: T6,
|
||||
callback: (err: any, result: R) => any
|
||||
) => any
|
||||
): (v1: T, v2: T2, v3: T3, v4: T4, v5: T5, v6: T6) => Observable<R>;
|
||||
|
||||
/**
|
||||
* Converts a Node.js-style callback API to a function that returns an
|
||||
* Observable.
|
||||
*
|
||||
* Does NOT handle functions whose callbacks have
|
||||
* more than two parameters. Only the first value after the
|
||||
* error argument will be returned.
|
||||
*
|
||||
* Example: Read a file from the filesystem and get the data as an Observable:
|
||||
*
|
||||
* import fs from 'fs';
|
||||
* var readFileAsObservable = $bindNodeCallback(fs.readFile);
|
||||
* var result = readFileAsObservable('./roadNames.txt', 'utf8');
|
||||
* result.subscribe(
|
||||
* x => console.log(x),
|
||||
* e => console.error(e)
|
||||
* );
|
||||
*/
|
||||
export function $bindNodeCallback<T>(callbackFunc: (...args: any[]) => any) {
|
||||
return function(this: any, ...args: any[]): Observable<T> {
|
||||
const context = this;
|
||||
|
||||
return new Observable(observer => {
|
||||
function handlerFn(err?: Error, val?: T, ...rest: any[]) {
|
||||
if (err != null) {
|
||||
observer.error(err);
|
||||
} else if (rest.length > 0) {
|
||||
// If we've received more than two arguments, the function doesn't
|
||||
// follow the common Node.js callback style. We could return an array
|
||||
// if that happened, but as most code follow the pattern we don't
|
||||
// special case it for now.
|
||||
observer.error(new Error('Node callback called with too many args'));
|
||||
} else {
|
||||
observer.next(val!);
|
||||
observer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
callbackFunc.apply(context, args.concat([handlerFn]));
|
||||
});
|
||||
};
|
||||
}
|
113
src/core/lib/kbn_observable/factories/combine_latest.ts
Normal file
113
src/core/lib/kbn_observable/factories/combine_latest.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable, ObservableInput } from '../observable';
|
||||
import { $from } from './from';
|
||||
|
||||
const pending = Symbol('awaiting first value');
|
||||
|
||||
export function $combineLatest<T, T2>(
|
||||
v1: ObservableInput<T>,
|
||||
v2: ObservableInput<T2>
|
||||
): Observable<[T, T2]>;
|
||||
export function $combineLatest<T, T2, T3>(
|
||||
v1: ObservableInput<T>,
|
||||
v2: ObservableInput<T2>,
|
||||
v3: ObservableInput<T3>
|
||||
): Observable<[T, T2, T3]>;
|
||||
export function $combineLatest<T, T2, T3, T4>(
|
||||
v1: ObservableInput<T>,
|
||||
v2: ObservableInput<T2>,
|
||||
v3: ObservableInput<T3>,
|
||||
v4: ObservableInput<T4>
|
||||
): Observable<[T, T2, T3, T4]>;
|
||||
export function $combineLatest<T, T2, T3, T4, T5>(
|
||||
v1: ObservableInput<T>,
|
||||
v2: ObservableInput<T2>,
|
||||
v3: ObservableInput<T3>,
|
||||
v4: ObservableInput<T4>,
|
||||
v5: ObservableInput<T5>
|
||||
): Observable<[T, T2, T3, T4, T5]>;
|
||||
export function $combineLatest<T, T2, T3, T4, T5, T6>(
|
||||
v1: ObservableInput<T>,
|
||||
v2: ObservableInput<T2>,
|
||||
v3: ObservableInput<T3>,
|
||||
v4: ObservableInput<T4>,
|
||||
v5: ObservableInput<T5>,
|
||||
v6: ObservableInput<T6>
|
||||
): Observable<[T, T2, T3, T4, T5, T6]>;
|
||||
export function $combineLatest<T>(...observables: Array<ObservableInput<T>>): Observable<T[]>;
|
||||
|
||||
/**
|
||||
* Creates an observable that combines the values by subscribing to all
|
||||
* observables passed and emiting an array with the latest value from each
|
||||
* observable once after each observable has emitted at least once, and again
|
||||
* any time an observable emits after that.
|
||||
*
|
||||
* @param {Observable...}
|
||||
* @return {Observable}
|
||||
*/
|
||||
export function $combineLatest<T>(...observables: Array<ObservableInput<T>>): Observable<T[]> {
|
||||
return new Observable(observer => {
|
||||
// create an array that will receive values as observables
|
||||
// update and initialize it with `pending` symbols so that
|
||||
// we know when observables emit for the first time
|
||||
const values: Array<symbol | T> = observables.map(() => pending);
|
||||
|
||||
let needFirstCount = values.length;
|
||||
let activeCount = values.length;
|
||||
|
||||
const subs = observables.map((observable, i) =>
|
||||
$from(observable).subscribe({
|
||||
next(value) {
|
||||
if (values[i] === pending) {
|
||||
needFirstCount--;
|
||||
}
|
||||
|
||||
values[i] = value;
|
||||
|
||||
if (needFirstCount === 0) {
|
||||
observer.next(values.slice() as T[]);
|
||||
}
|
||||
},
|
||||
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
values.length = 0;
|
||||
},
|
||||
|
||||
complete() {
|
||||
activeCount--;
|
||||
|
||||
if (activeCount === 0) {
|
||||
observer.complete();
|
||||
values.length = 0;
|
||||
}
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
subs.forEach(sub => {
|
||||
sub.unsubscribe();
|
||||
});
|
||||
values.length = 0;
|
||||
};
|
||||
});
|
||||
}
|
64
src/core/lib/kbn_observable/factories/concat.ts
Normal file
64
src/core/lib/kbn_observable/factories/concat.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable, Subscription } from '../observable';
|
||||
|
||||
/**
|
||||
* Creates an observable that combines all observables passed as arguments into
|
||||
* a single output observable by subscribing to them in series, i.e. it will
|
||||
* subscribe to the next observable when the previous completes.
|
||||
*
|
||||
* @param {Observable...}
|
||||
* @return {Observable}
|
||||
*/
|
||||
export function $concat<T>(...observables: Array<Observable<T>>) {
|
||||
return new Observable(observer => {
|
||||
let subscription: Subscription | undefined;
|
||||
|
||||
function subscribe(i: number) {
|
||||
if (observer.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (i >= observables.length) {
|
||||
observer.complete();
|
||||
}
|
||||
|
||||
subscription = observables[i].subscribe({
|
||||
next(value) {
|
||||
observer.next(value);
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
subscribe(i + 1);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
subscribe(0);
|
||||
|
||||
return () => {
|
||||
if (subscription !== undefined) {
|
||||
subscription.unsubscribe();
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
26
src/core/lib/kbn_observable/factories/error.ts
Normal file
26
src/core/lib/kbn_observable/factories/error.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from '../observable';
|
||||
|
||||
export function $error<E extends Error>(error: E) {
|
||||
return new Observable(observer => {
|
||||
observer.error(error);
|
||||
});
|
||||
}
|
32
src/core/lib/kbn_observable/factories/from.ts
Normal file
32
src/core/lib/kbn_observable/factories/from.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable, ObservableInput } from '../observable';
|
||||
|
||||
/**
|
||||
* Alias for `Observable.from`
|
||||
*
|
||||
* If you need to handle:
|
||||
*
|
||||
* - promises, use `$fromPromise`
|
||||
* - functions, use `$fromCallback`
|
||||
*/
|
||||
export function $from<T>(x: ObservableInput<T>): Observable<T> {
|
||||
return Observable.from(x);
|
||||
}
|
48
src/core/lib/kbn_observable/factories/from_callback.ts
Normal file
48
src/core/lib/kbn_observable/factories/from_callback.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { isObservable } from '../lib/is_observable';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Creates an observable that calls the specified function with no arguments
|
||||
* when it is subscribed. The observerable will behave differently based on the
|
||||
* return value of the factory:
|
||||
*
|
||||
* - return `undefined`: observable will immediately complete
|
||||
* - returns observable: observerable will mirror the returned value
|
||||
* - otherwise: observable will emit the value and then complete
|
||||
*
|
||||
* @param {Function}
|
||||
* @returns {Observable}
|
||||
*/
|
||||
export function $fromCallback<T>(factory: () => T | Observable<T>): Observable<T> {
|
||||
return new Observable(observer => {
|
||||
const result = factory();
|
||||
|
||||
if (result === undefined) {
|
||||
observer.complete();
|
||||
} else if (isObservable(result)) {
|
||||
return result.subscribe(observer);
|
||||
} else {
|
||||
observer.next(result);
|
||||
observer.complete();
|
||||
}
|
||||
});
|
||||
}
|
42
src/core/lib/kbn_observable/factories/from_promise.ts
Normal file
42
src/core/lib/kbn_observable/factories/from_promise.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Create an observable that mirrors a promise. If the promise resolves the
|
||||
* observable will emit the resolved value and then complete. If it rejects then
|
||||
* the observable will error.
|
||||
*
|
||||
* @param {Promise<T>}
|
||||
* @return {Observable<T>}
|
||||
*/
|
||||
export function $fromPromise<T>(promise: Promise<T>): Observable<T> {
|
||||
return new Observable(observer => {
|
||||
promise.then(
|
||||
value => {
|
||||
observer.next(value);
|
||||
observer.complete();
|
||||
},
|
||||
error => {
|
||||
observer.error(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
27
src/core/lib/kbn_observable/factories/index.ts
Normal file
27
src/core/lib/kbn_observable/factories/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { $from } from './from';
|
||||
export { $combineLatest } from './combine_latest';
|
||||
export { $concat } from './concat';
|
||||
export { $fromCallback } from './from_callback';
|
||||
export { $bindNodeCallback } from './bind_node_callback';
|
||||
export { $fromPromise } from './from_promise';
|
||||
export { $of } from './of';
|
||||
export { $error } from './error';
|
27
src/core/lib/kbn_observable/factories/of.ts
Normal file
27
src/core/lib/kbn_observable/factories/of.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Alias for `Observable.of`
|
||||
*/
|
||||
export function $of<T>(...items: T[]): Observable<T> {
|
||||
return Observable.of(...items);
|
||||
}
|
27
src/core/lib/kbn_observable/index.ts
Normal file
27
src/core/lib/kbn_observable/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { k$ } from './k';
|
||||
|
||||
export * from './observable';
|
||||
export { Subject } from './subject';
|
||||
export { BehaviorSubject } from './behavior_subject';
|
||||
|
||||
export * from './operators';
|
||||
export * from './factories';
|
26
src/core/lib/kbn_observable/interfaces.ts
Normal file
26
src/core/lib/kbn_observable/interfaces.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from './observable';
|
||||
|
||||
export type UnaryFunction<T, R> = (source: T) => R;
|
||||
|
||||
export type OperatorFunction<T, R> = UnaryFunction<Observable<T>, Observable<R>>;
|
||||
|
||||
export type MonoTypeOperatorFunction<T> = OperatorFunction<T, T>;
|
90
src/core/lib/kbn_observable/k.ts
Normal file
90
src/core/lib/kbn_observable/k.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $from } from './factories';
|
||||
import { UnaryFunction } from './interfaces';
|
||||
import { pipeFromArray } from './lib';
|
||||
import { Observable, ObservableInput } from './observable';
|
||||
|
||||
export function k$<T, R>(source: ObservableInput<T>) {
|
||||
function kOperations<A>(op1: UnaryFunction<Observable<T>, A>): A;
|
||||
function kOperations<A, B>(op1: UnaryFunction<Observable<T>, A>, op2: UnaryFunction<A, B>): B;
|
||||
function kOperations<A, B, C>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>
|
||||
): C;
|
||||
function kOperations<A, B, C, D>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>
|
||||
): D;
|
||||
function kOperations<A, B, C, D, E>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>
|
||||
): E;
|
||||
function kOperations<A, B, C, D, E, F>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>
|
||||
): F;
|
||||
function kOperations<A, B, C, D, E, F, G>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>
|
||||
): G;
|
||||
function kOperations<A, B, C, D, E, F, G, H>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>,
|
||||
op8: UnaryFunction<G, H>
|
||||
): H;
|
||||
function kOperations<A, B, C, D, E, F, G, H, I>(
|
||||
op1: UnaryFunction<Observable<T>, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>,
|
||||
op8: UnaryFunction<G, H>,
|
||||
op9: UnaryFunction<H, I>
|
||||
): I;
|
||||
|
||||
function kOperations(...operations: Array<UnaryFunction<Observable<T>, R>>) {
|
||||
return pipeFromArray(operations)($from(source));
|
||||
}
|
||||
|
||||
return kOperations;
|
||||
}
|
|
@ -17,33 +17,29 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import Boom from 'boom';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
import { modifyUrl } from '../../utils';
|
||||
/**
|
||||
* Test helper that collects all actions, and returns an array with all
|
||||
* `next`-ed values, plus any `error` received or a `C` if `complete` is
|
||||
* triggered.
|
||||
*/
|
||||
export function collect<T>(source: Observable<T>) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const values: any[] = [];
|
||||
|
||||
export function setupBasePathRewrite(server, config) {
|
||||
const basePath = config.get('server.basePath');
|
||||
const rewriteBasePath = config.get('server.rewriteBasePath');
|
||||
|
||||
if (!basePath || !rewriteBasePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
server.ext('onRequest', (request, reply) => {
|
||||
const newUrl = modifyUrl(request.url.href, parsed => {
|
||||
if (parsed.pathname.startsWith(basePath)) {
|
||||
parsed.pathname = parsed.pathname.replace(basePath, '') || '/';
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
source.subscribe({
|
||||
next(x) {
|
||||
values.push(x);
|
||||
},
|
||||
error(err) {
|
||||
values.push(err);
|
||||
resolve(values);
|
||||
},
|
||||
complete() {
|
||||
values.push('C');
|
||||
resolve(values);
|
||||
},
|
||||
});
|
||||
|
||||
if (!newUrl) {
|
||||
reply(Boom.notFound());
|
||||
return;
|
||||
}
|
||||
|
||||
request.setUrl(newUrl);
|
||||
reply.continue();
|
||||
});
|
||||
}
|
20
src/core/lib/kbn_observable/lib/index.ts
Normal file
20
src/core/lib/kbn_observable/lib/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { pipe, pipeFromArray } from './pipe';
|
24
src/core/lib/kbn_observable/lib/is_observable.ts
Normal file
24
src/core/lib/kbn_observable/lib/is_observable.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable } from '../observable';
|
||||
|
||||
export function isObservable<T>(x: any): x is Observable<T> {
|
||||
return x !== null && typeof x === 'object' && x[Symbol.observable] !== undefined;
|
||||
}
|
106
src/core/lib/kbn_observable/lib/pipe.ts
Normal file
106
src/core/lib/kbn_observable/lib/pipe.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { UnaryFunction } from '../interfaces';
|
||||
|
||||
export function pipe<T>(): UnaryFunction<T, T>;
|
||||
export function pipe<T, A>(op1: UnaryFunction<T, A>): UnaryFunction<T, A>;
|
||||
export function pipe<T, A, B>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>
|
||||
): UnaryFunction<T, B>;
|
||||
export function pipe<T, A, B, C>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>
|
||||
): UnaryFunction<T, C>;
|
||||
export function pipe<T, A, B, C, D>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>
|
||||
): UnaryFunction<T, D>;
|
||||
export function pipe<T, A, B, C, D, E>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>
|
||||
): UnaryFunction<T, E>;
|
||||
export function pipe<T, A, B, C, D, E, F>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>
|
||||
): UnaryFunction<T, F>;
|
||||
export function pipe<T, A, B, C, D, E, F, G>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>
|
||||
): UnaryFunction<T, G>;
|
||||
export function pipe<T, A, B, C, D, E, F, G, H>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>,
|
||||
op8: UnaryFunction<G, H>
|
||||
): UnaryFunction<T, H>;
|
||||
export function pipe<T, A, B, C, D, E, F, G, H, I>(
|
||||
op1: UnaryFunction<T, A>,
|
||||
op2: UnaryFunction<A, B>,
|
||||
op3: UnaryFunction<B, C>,
|
||||
op4: UnaryFunction<C, D>,
|
||||
op5: UnaryFunction<D, E>,
|
||||
op6: UnaryFunction<E, F>,
|
||||
op7: UnaryFunction<F, G>,
|
||||
op8: UnaryFunction<G, H>,
|
||||
op9: UnaryFunction<H, I>
|
||||
): UnaryFunction<T, I>;
|
||||
|
||||
export function pipe<T, R>(...fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {
|
||||
return pipeFromArray(fns);
|
||||
}
|
||||
|
||||
const noop: () => any = () => {
|
||||
// noop
|
||||
};
|
||||
|
||||
/* @internal */
|
||||
export function pipeFromArray<T, R>(fns: Array<UnaryFunction<T, R>>): UnaryFunction<T, R> {
|
||||
if (fns.length === 0) {
|
||||
return noop as UnaryFunction<T, R>;
|
||||
}
|
||||
|
||||
if (fns.length === 1) {
|
||||
return fns[0];
|
||||
}
|
||||
|
||||
return function piped(input: T): R {
|
||||
return fns.reduce((prev: any, fn) => fn(prev), input);
|
||||
};
|
||||
}
|
28
src/core/lib/kbn_observable/observable.ts
Normal file
28
src/core/lib/kbn_observable/observable.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
Observable,
|
||||
ObservableInput,
|
||||
Subscription,
|
||||
Subscribable,
|
||||
SubscriptionObserver,
|
||||
Observer,
|
||||
PartialObserver,
|
||||
} from '../kbn_internal_native_observable';
|
|
@ -0,0 +1,7 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`returns error if completing without receiving any value 1`] = `
|
||||
Array [
|
||||
[Error: EmptyError: first() requires source stream to emit at least one value.],
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`returns error if completing without receiving any value 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
[Error: EmptyError: last() requires source stream to emit at least one value.],
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`rejects if error received 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
[Error: fail],
|
||||
],
|
||||
]
|
||||
`;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { filter } from '../';
|
||||
import { $from } from '../../factories';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
|
||||
const number$ = $from([1, 2, 3]);
|
||||
|
||||
test('returns the filtered values', async () => {
|
||||
const filter$ = k$(number$)(filter(n => n > 1));
|
||||
|
||||
const res = collect(filter$);
|
||||
expect(await res).toEqual([2, 3, 'C']);
|
||||
});
|
||||
|
||||
test('sends the index as arg 2', async () => {
|
||||
const filter$ = k$(number$)(filter((n, i) => i > 1));
|
||||
|
||||
const res = collect(filter$);
|
||||
expect(await res).toEqual([3, 'C']);
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { first } from '../';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('returns the first value, then completes', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const observable = k$(values$)(first());
|
||||
const res = collect(observable);
|
||||
|
||||
values$.next('foo');
|
||||
values$.next('bar');
|
||||
|
||||
expect(await res).toEqual(['foo', 'C']);
|
||||
});
|
||||
|
||||
test('handles source completing after receiving value', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const observable = k$(values$)(first());
|
||||
const res = collect(observable);
|
||||
|
||||
values$.next('foo');
|
||||
values$.next('bar');
|
||||
values$.complete();
|
||||
|
||||
expect(await res).toEqual(['foo', 'C']);
|
||||
});
|
||||
|
||||
test('returns error if completing without receiving any value', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const observable = k$(values$)(first());
|
||||
const res = collect(observable);
|
||||
|
||||
values$.complete();
|
||||
|
||||
expect(await res).toMatchSnapshot();
|
||||
});
|
60
src/core/lib/kbn_observable/operators/__tests__/last.test.ts
Normal file
60
src/core/lib/kbn_observable/operators/__tests__/last.test.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { last } from '../';
|
||||
import { k$ } from '../../k';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('returns the last value', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const next = jest.fn();
|
||||
const error = jest.fn();
|
||||
const complete = jest.fn();
|
||||
|
||||
k$(values$)(last()).subscribe(next, error, complete);
|
||||
|
||||
values$.next('foo');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
|
||||
values$.next('bar');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
|
||||
values$.complete();
|
||||
|
||||
expect(next).toHaveBeenCalledTimes(1);
|
||||
expect(next).toHaveBeenCalledWith('bar');
|
||||
expect(error).not.toHaveBeenCalled();
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('returns error if completing without receiving any value', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const error = jest.fn();
|
||||
|
||||
k$(values$)(last()).subscribe({
|
||||
error,
|
||||
});
|
||||
|
||||
values$.complete();
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error.mock.calls).toMatchSnapshot();
|
||||
});
|
38
src/core/lib/kbn_observable/operators/__tests__/map.test.ts
Normal file
38
src/core/lib/kbn_observable/operators/__tests__/map.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { map, toArray, toPromise } from '../';
|
||||
import { $from } from '../../factories';
|
||||
import { k$ } from '../../k';
|
||||
import { Observable } from '../../observable';
|
||||
|
||||
const number$ = $from([1, 2, 3]);
|
||||
const collect = <T>(source: Observable<T>) => k$(source)(toArray(), toPromise());
|
||||
|
||||
test('returns the modified value', async () => {
|
||||
const numbers = await collect(k$(number$)(map(n => n * 1000)));
|
||||
|
||||
expect(numbers).toEqual([1000, 2000, 3000]);
|
||||
});
|
||||
|
||||
test('sends the index as arg 2', async () => {
|
||||
const numbers = await collect(k$(number$)(map((n, i) => i)));
|
||||
|
||||
expect(numbers).toEqual([0, 1, 2]);
|
||||
});
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { map, mergeMap } from '../';
|
||||
import { $error, $of } from '../../factories';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Observable } from '../../observable';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
const tickMs = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
test('should mergeMap many outer values to many inner values', async () => {
|
||||
const inner$ = new Subject();
|
||||
|
||||
const outer$ = Observable.from([1, 2, 3, 4]);
|
||||
const project = (value: number) => k$(inner$)(map(x => `${value}-${x}`));
|
||||
|
||||
const observable = k$(outer$)(mergeMap(project));
|
||||
const res = collect(observable);
|
||||
|
||||
await tickMs(10);
|
||||
inner$.next('a');
|
||||
|
||||
await tickMs(10);
|
||||
inner$.next('b');
|
||||
|
||||
await tickMs(10);
|
||||
inner$.next('c');
|
||||
|
||||
inner$.complete();
|
||||
|
||||
expect(await res).toEqual([
|
||||
'1-a',
|
||||
'2-a',
|
||||
'3-a',
|
||||
'4-a',
|
||||
'1-b',
|
||||
'2-b',
|
||||
'3-b',
|
||||
'4-b',
|
||||
'1-c',
|
||||
'2-c',
|
||||
'3-c',
|
||||
'4-c',
|
||||
'C',
|
||||
]);
|
||||
});
|
||||
|
||||
test('should mergeMap many outer values to many inner values, early complete', async () => {
|
||||
const outer$ = new Subject<number>();
|
||||
const inner$ = new Subject();
|
||||
|
||||
const project = (value: number) => k$(inner$)(map(x => `${value}-${x}`));
|
||||
|
||||
const observable = k$(outer$)(mergeMap(project));
|
||||
const res = collect(observable);
|
||||
|
||||
outer$.next(1);
|
||||
outer$.next(2);
|
||||
outer$.complete();
|
||||
|
||||
// This shouldn't end up in the results because `outer$` has completed.
|
||||
outer$.next(3);
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('a');
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('b');
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('c');
|
||||
|
||||
inner$.complete();
|
||||
|
||||
expect(await res).toEqual(['1-a', '2-a', '1-b', '2-b', '1-c', '2-c', 'C']);
|
||||
});
|
||||
|
||||
test('should mergeMap many outer to many inner, and inner throws', async () => {
|
||||
const source = Observable.from([1, 2, 3, 4]);
|
||||
const error = new Error('fail');
|
||||
|
||||
const project = (value: number, index: number) => (index > 1 ? $error(error) : $of(value));
|
||||
|
||||
const observable = k$(source)(mergeMap(project));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual([1, 2, error]);
|
||||
});
|
||||
|
||||
test('should mergeMap many outer to many inner, and outer throws', async () => {
|
||||
const outer$ = new Subject<number>();
|
||||
const inner$ = new Subject();
|
||||
|
||||
const project = (value: number) => k$(inner$)(map(x => `${value}-${x}`));
|
||||
|
||||
const observable = k$(outer$)(mergeMap(project));
|
||||
const res = collect(observable);
|
||||
|
||||
outer$.next(1);
|
||||
outer$.next(2);
|
||||
|
||||
const error = new Error('outer fails');
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('a');
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('b');
|
||||
|
||||
outer$.error(error);
|
||||
// This shouldn't end up in the results because `outer$` has failed
|
||||
outer$.next(3);
|
||||
|
||||
await tickMs(5);
|
||||
inner$.next('c');
|
||||
|
||||
expect(await res).toEqual(['1-a', '2-a', '1-b', '2-b', error]);
|
||||
});
|
||||
|
||||
test('should mergeMap many outer to an array for each value', async () => {
|
||||
const source = Observable.from([1, 2, 3]);
|
||||
|
||||
const observable = k$(source)(mergeMap(() => $of('a', 'b', 'c')));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual(['a', 'b', 'c', 'a', 'b', 'c', 'a', 'b', 'c', 'C']);
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { reduce } from '../';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('completes when source completes', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
reduce((acc, val) => {
|
||||
return acc + val;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
subject.next('bar');
|
||||
subject.next('baz');
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foobarbaz', 'C']);
|
||||
});
|
||||
|
||||
test('injects index', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
reduce((acc, val, index) => {
|
||||
return acc + index;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
subject.next('bar');
|
||||
subject.next('baz');
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foo01', 'C']);
|
||||
});
|
||||
|
||||
test('completes with initial value if no values received', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
reduce((acc, val, index) => {
|
||||
return acc + val;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foo', 'C']);
|
||||
});
|
72
src/core/lib/kbn_observable/operators/__tests__/scan.test.ts
Normal file
72
src/core/lib/kbn_observable/operators/__tests__/scan.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { scan } from '../';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('completes when source completes', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
scan((acc, val) => {
|
||||
return acc + val;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
subject.next('bar');
|
||||
subject.next('baz');
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foobar', 'foobarbaz', 'C']);
|
||||
});
|
||||
|
||||
test('injects index', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
scan((acc, val, index) => {
|
||||
return acc + index;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
subject.next('bar');
|
||||
subject.next('baz');
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['foo0', 'foo01', 'C']);
|
||||
});
|
||||
|
||||
test('completes if no values received', async () => {
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const observable = k$(subject)(
|
||||
scan((acc, val, index) => {
|
||||
return acc + val;
|
||||
}, 'foo')
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
subject.complete();
|
||||
|
||||
expect(await res).toEqual(['C']);
|
||||
});
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { skipRepeats } from '../';
|
||||
import { $of } from '../../factories';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Observable } from '../../observable';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
test('should distinguish between values', async () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const observable = k$(values$)(skipRepeats());
|
||||
const res = collect(observable);
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('b');
|
||||
values$.next('b');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.complete();
|
||||
|
||||
expect(await res).toEqual(['a', 'b', 'a', 'C']);
|
||||
});
|
||||
|
||||
test('should distinguish between values and does not complete', () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats()).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('b');
|
||||
values$.next('b');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
|
||||
expect(actual).toEqual(['a', 'b', 'a']);
|
||||
});
|
||||
|
||||
test('should complete if source is empty', done => {
|
||||
const values$ = $of();
|
||||
|
||||
k$(values$)(skipRepeats()).subscribe({
|
||||
complete: done,
|
||||
});
|
||||
});
|
||||
|
||||
test('should emit if source emits single element only', () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats()).subscribe({
|
||||
next(x) {
|
||||
actual.push(x);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next('a');
|
||||
|
||||
expect(actual).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('should emit if source is scalar', () => {
|
||||
const values$ = $of('a');
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats()).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
expect(actual).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('should raise error if source raises error', async () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const observable = k$(values$)(skipRepeats());
|
||||
const res = collect(observable);
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
|
||||
const thrownError = new Error('nope');
|
||||
values$.error(thrownError);
|
||||
|
||||
expect(await res).toEqual(['a', thrownError]);
|
||||
});
|
||||
|
||||
test('should raise error if source throws', () => {
|
||||
const thrownError = new Error('fail');
|
||||
|
||||
const obs = new Observable(observer => {
|
||||
observer.error(thrownError);
|
||||
});
|
||||
|
||||
const error = jest.fn();
|
||||
k$(obs)(skipRepeats()).subscribe({
|
||||
error,
|
||||
});
|
||||
|
||||
expect(error).toHaveBeenCalledWith(thrownError);
|
||||
});
|
||||
|
||||
test('should allow unsubscribing early and explicitly', () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const actual: any[] = [];
|
||||
const sub = k$(values$)(skipRepeats()).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('b');
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
values$.next('c');
|
||||
values$.next('d');
|
||||
|
||||
expect(actual).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
test('should emit once if comparator returns true always regardless of source emits', () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats(() => true)).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('b');
|
||||
values$.next('c');
|
||||
|
||||
expect(actual).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('should emit all if comparator returns false always regardless of source emits', () => {
|
||||
const values$ = new Subject<string>();
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats(() => false)).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
values$.next('a');
|
||||
|
||||
expect(actual).toEqual(['a', 'a', 'a', 'a']);
|
||||
});
|
||||
|
||||
test('should distinguish values by comparator', () => {
|
||||
const values$ = new Subject<number>();
|
||||
|
||||
const comparator = (x: number, y: number) => y % 2 === 0;
|
||||
|
||||
const actual: any[] = [];
|
||||
k$(values$)(skipRepeats(comparator)).subscribe({
|
||||
next(v) {
|
||||
actual.push(v);
|
||||
},
|
||||
});
|
||||
|
||||
values$.next(1);
|
||||
values$.next(2);
|
||||
values$.next(3);
|
||||
values$.next(4);
|
||||
|
||||
expect(actual).toEqual([1, 3]);
|
||||
});
|
|
@ -0,0 +1,302 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { switchMap } from '../';
|
||||
import { $of } from '../../factories';
|
||||
import { k$ } from '../../k';
|
||||
import { collect } from '../../lib/collect';
|
||||
import { Observable } from '../../observable';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
const number$ = $of(1, 2, 3);
|
||||
|
||||
test('returns the modified value', async () => {
|
||||
const expected = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'a3', 'b3', 'c3', 'C'];
|
||||
|
||||
const observable = k$(number$)(switchMap(x => $of('a' + x, 'b' + x, 'c' + x)));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual(expected);
|
||||
});
|
||||
|
||||
test('injects index to map', async () => {
|
||||
const observable = k$(number$)(switchMap((x, i) => $of(i)));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual([0, 1, 2, 'C']);
|
||||
});
|
||||
|
||||
test('should unsubscribe inner observable when source observable emits new value', async () => {
|
||||
const unsubbed: string[] = [];
|
||||
const subject = new Subject<string>();
|
||||
|
||||
k$(subject)(
|
||||
switchMap(
|
||||
x =>
|
||||
new Observable(observer => {
|
||||
return () => {
|
||||
unsubbed.push(x);
|
||||
};
|
||||
})
|
||||
)
|
||||
).subscribe();
|
||||
|
||||
subject.next('a');
|
||||
expect(unsubbed).toEqual([]);
|
||||
|
||||
subject.next('b');
|
||||
expect(unsubbed).toEqual(['a']);
|
||||
|
||||
subject.next('c');
|
||||
expect(unsubbed).toEqual(['a', 'b']);
|
||||
|
||||
subject.complete();
|
||||
expect(unsubbed).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
test('should unsubscribe inner observable when source observable errors', async () => {
|
||||
const unsubbed: string[] = [];
|
||||
const subject = new Subject<string>();
|
||||
|
||||
k$(subject)(
|
||||
switchMap(
|
||||
x =>
|
||||
new Observable(observer => {
|
||||
return () => {
|
||||
unsubbed.push(x);
|
||||
};
|
||||
})
|
||||
)
|
||||
).subscribe();
|
||||
|
||||
subject.next('a');
|
||||
subject.error(new Error('fail'));
|
||||
|
||||
expect(unsubbed).toEqual(['a']);
|
||||
});
|
||||
|
||||
test('should unsubscribe inner observables if inner observer completes', async () => {
|
||||
const unsubbed: string[] = [];
|
||||
const subject = new Subject<string>();
|
||||
|
||||
k$(subject)(
|
||||
switchMap(
|
||||
x =>
|
||||
new Observable(observer => {
|
||||
observer.complete();
|
||||
return () => {
|
||||
unsubbed.push(x);
|
||||
};
|
||||
})
|
||||
)
|
||||
).subscribe();
|
||||
|
||||
subject.next('a');
|
||||
expect(unsubbed).toEqual(['a']);
|
||||
|
||||
subject.next('b');
|
||||
expect(unsubbed).toEqual(['a', 'b']);
|
||||
|
||||
subject.complete();
|
||||
expect(unsubbed).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
test('should unsubscribe inner observables if inner observer errors', async () => {
|
||||
const unsubbed: string[] = [];
|
||||
const subject = new Subject<string>();
|
||||
|
||||
const error = jest.fn();
|
||||
const thrownError = new Error('fail');
|
||||
|
||||
k$(subject)(
|
||||
switchMap(
|
||||
x =>
|
||||
new Observable(observer => {
|
||||
observer.error(thrownError);
|
||||
return () => {
|
||||
unsubbed.push(x);
|
||||
};
|
||||
})
|
||||
)
|
||||
).subscribe({
|
||||
error,
|
||||
});
|
||||
|
||||
subject.next('a');
|
||||
expect(unsubbed).toEqual(['a']);
|
||||
|
||||
expect(error).toHaveBeenCalledTimes(1);
|
||||
expect(error).toHaveBeenCalledWith(thrownError);
|
||||
});
|
||||
|
||||
test('should switch inner observables', () => {
|
||||
const outer$ = new Subject<'x' | 'y'>();
|
||||
const inner$ = {
|
||||
x: new Subject(),
|
||||
y: new Subject(),
|
||||
};
|
||||
|
||||
const actual: any[] = [];
|
||||
|
||||
k$(outer$)(switchMap(x => inner$[x])).subscribe({
|
||||
next(val) {
|
||||
actual.push(val);
|
||||
},
|
||||
});
|
||||
|
||||
outer$.next('x');
|
||||
inner$.x.next('foo');
|
||||
inner$.x.next('bar');
|
||||
|
||||
outer$.next('y');
|
||||
inner$.x.next('baz');
|
||||
inner$.y.next('quux');
|
||||
|
||||
outer$.complete();
|
||||
|
||||
expect(actual).toEqual(['foo', 'bar', 'quux']);
|
||||
});
|
||||
|
||||
test('should switch inner empty and empty', () => {
|
||||
const outer$ = new Subject<'x' | 'y'>();
|
||||
const inner$ = {
|
||||
x: new Subject(),
|
||||
y: new Subject(),
|
||||
};
|
||||
|
||||
const next = jest.fn();
|
||||
|
||||
k$(outer$)(switchMap(x => inner$[x])).subscribe(next);
|
||||
|
||||
outer$.next('x');
|
||||
inner$.x.complete();
|
||||
|
||||
outer$.next('y');
|
||||
inner$.y.complete();
|
||||
|
||||
outer$.complete();
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should switch inner never and throw', async () => {
|
||||
const error = new Error('sad');
|
||||
|
||||
const outer$ = new Subject<'x' | 'y'>();
|
||||
const inner$ = {
|
||||
x: new Subject(),
|
||||
y: new Subject(),
|
||||
};
|
||||
|
||||
inner$.y.error(error);
|
||||
|
||||
const observable = k$(outer$)(switchMap(x => inner$[x]));
|
||||
const res = collect(observable);
|
||||
|
||||
outer$.next('x');
|
||||
outer$.next('y');
|
||||
outer$.complete();
|
||||
|
||||
expect(await res).toEqual([error]);
|
||||
});
|
||||
|
||||
test('should handle outer throw', async () => {
|
||||
const error = new Error('foo');
|
||||
const outer$ = new Observable<string>(observer => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
const observable = k$(outer$)(switchMap(x => $of(x)));
|
||||
const res = collect(observable);
|
||||
|
||||
expect(await res).toEqual([error]);
|
||||
});
|
||||
|
||||
test('should handle outer error', async () => {
|
||||
const outer$ = new Subject<'x'>();
|
||||
const inner$ = {
|
||||
x: new Subject(),
|
||||
};
|
||||
|
||||
const observable = k$(outer$)(switchMap(x => inner$[x]));
|
||||
const res = collect(observable);
|
||||
|
||||
outer$.next('x');
|
||||
|
||||
inner$.x.next('a');
|
||||
inner$.x.next('b');
|
||||
inner$.x.next('c');
|
||||
|
||||
const error = new Error('foo');
|
||||
outer$.error(error);
|
||||
|
||||
inner$.x.next('d');
|
||||
inner$.x.next('e');
|
||||
|
||||
expect(await res).toEqual(['a', 'b', 'c', error]);
|
||||
});
|
||||
|
||||
test('should raise error when projection throws', async () => {
|
||||
const outer$ = new Subject<string>();
|
||||
const error = new Error('foo');
|
||||
|
||||
const observable = k$(outer$)(
|
||||
switchMap(x => {
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
const res = collect(observable);
|
||||
|
||||
outer$.next('x');
|
||||
|
||||
expect(await res).toEqual([error]);
|
||||
});
|
||||
|
||||
test('should switch inner cold observables, outer is unsubscribed early', () => {
|
||||
const outer$ = new Subject<'x' | 'y'>();
|
||||
const inner$ = {
|
||||
x: new Subject(),
|
||||
y: new Subject(),
|
||||
};
|
||||
|
||||
const actual: any[] = [];
|
||||
const sub = k$(outer$)(switchMap(x => inner$[x])).subscribe({
|
||||
next(val) {
|
||||
actual.push(val);
|
||||
},
|
||||
});
|
||||
|
||||
outer$.next('x');
|
||||
inner$.x.next('foo');
|
||||
inner$.x.next('bar');
|
||||
|
||||
outer$.next('y');
|
||||
inner$.y.next('baz');
|
||||
inner$.y.next('quux');
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
inner$.x.next('post x');
|
||||
inner$.x.complete();
|
||||
|
||||
inner$.y.next('post y');
|
||||
inner$.y.complete();
|
||||
|
||||
expect(actual).toEqual(['foo', 'bar', 'baz', 'quux']);
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { toPromise } from '../';
|
||||
import { k$ } from '../../k';
|
||||
import { Subject } from '../../subject';
|
||||
|
||||
// Promises are always async, so we add a simple helper that we can `await` to
|
||||
// make sure they have completed.
|
||||
const tick = () => Promise.resolve();
|
||||
|
||||
test('returns the last value', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const resolved = jest.fn();
|
||||
const rejected = jest.fn();
|
||||
|
||||
k$(values$)(toPromise()).then(resolved, rejected);
|
||||
|
||||
values$.next('foo');
|
||||
await tick();
|
||||
|
||||
expect(resolved).not.toHaveBeenCalled();
|
||||
expect(rejected).not.toHaveBeenCalled();
|
||||
|
||||
values$.next('bar');
|
||||
await tick();
|
||||
|
||||
expect(resolved).not.toHaveBeenCalled();
|
||||
expect(rejected).not.toHaveBeenCalled();
|
||||
|
||||
values$.complete();
|
||||
await tick();
|
||||
|
||||
expect(resolved).toHaveBeenCalledTimes(1);
|
||||
expect(resolved).toHaveBeenCalledWith('bar');
|
||||
expect(rejected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('resolves even if no values received', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const resolved = jest.fn();
|
||||
const rejected = jest.fn();
|
||||
|
||||
k$(values$)(toPromise()).then(resolved, rejected);
|
||||
|
||||
values$.complete();
|
||||
await tick();
|
||||
|
||||
expect(rejected).not.toHaveBeenCalled();
|
||||
expect(resolved).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('rejects if error received', async () => {
|
||||
const values$ = new Subject();
|
||||
|
||||
const resolved = jest.fn();
|
||||
const rejected = jest.fn();
|
||||
|
||||
k$(values$)(toPromise()).then(resolved, rejected);
|
||||
|
||||
values$.error(new Error('fail'));
|
||||
await tick();
|
||||
|
||||
expect(resolved).not.toHaveBeenCalled();
|
||||
expect(rejected).toHaveBeenCalledTimes(1);
|
||||
expect(rejected.mock.calls).toMatchSnapshot();
|
||||
});
|
64
src/core/lib/kbn_observable/operators/filter.ts
Normal file
64
src/core/lib/kbn_observable/operators/filter.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { MonoTypeOperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Filter items emitted by the source Observable by only emitting those that
|
||||
* satisfy a specified predicate.
|
||||
*
|
||||
* @param predicate A function that evaluates each value emitted by the source
|
||||
* Observable. If it returns `true`, the value is emitted, if `false` the value
|
||||
* is not passed to the output Observable. The `index` parameter is the number
|
||||
* `i` for the i-th source emission that has happened since the subscription,
|
||||
* starting from the number `0`.
|
||||
* @return An Observable of values from the source that were allowed by the
|
||||
* `predicate` function.
|
||||
*/
|
||||
export function filter<T>(
|
||||
predicate: (value: T, index: number) => boolean
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return function filterOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let i = 0;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
let result = false;
|
||||
try {
|
||||
result = predicate(value, i++);
|
||||
} catch (e) {
|
||||
observer.error(e);
|
||||
return;
|
||||
}
|
||||
if (result) {
|
||||
observer.next(value);
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
51
src/core/lib/kbn_observable/operators/first.ts
Normal file
51
src/core/lib/kbn_observable/operators/first.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { EmptyError } from '../errors';
|
||||
import { MonoTypeOperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Emits the first value emitted by the source Observable, then immediately
|
||||
* completes.
|
||||
*
|
||||
* @throws {EmptyError} Delivers an EmptyError to the Observer's `error`
|
||||
* callback if the Observable completes before any `next` notification was sent.
|
||||
*
|
||||
* @returns An Observable of the first item received.
|
||||
*/
|
||||
export function first<T>(): MonoTypeOperatorFunction<T> {
|
||||
return function firstOperation(source) {
|
||||
return new Observable(observer => {
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
observer.next(value);
|
||||
observer.complete();
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
// The only time we end up here, is if we never received any values.
|
||||
observer.error(new EmptyError('first()'));
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
62
src/core/lib/kbn_observable/operators/if_empty.ts
Normal file
62
src/core/lib/kbn_observable/operators/if_empty.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $fromCallback } from '../factories';
|
||||
import { MonoTypeOperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Modifies a stream so that when the source completes without emitting any
|
||||
* values a new observable is created via `factory()` (see `$fromCallback`) that
|
||||
* will be mirrored to completion.
|
||||
*
|
||||
* @param factory
|
||||
* @return
|
||||
*/
|
||||
export function ifEmpty<T>(factory: () => T): MonoTypeOperatorFunction<T> {
|
||||
return function ifEmptyOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let hasReceivedValue = false;
|
||||
|
||||
const subs = [
|
||||
source.subscribe({
|
||||
next(value) {
|
||||
hasReceivedValue = true;
|
||||
observer.next(value);
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
if (hasReceivedValue) {
|
||||
observer.complete();
|
||||
} else {
|
||||
subs.push($fromCallback(factory).subscribe(observer));
|
||||
}
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
return () => {
|
||||
subs.forEach(sub => sub.unsubscribe());
|
||||
subs.length = 0;
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
31
src/core/lib/kbn_observable/operators/index.ts
Normal file
31
src/core/lib/kbn_observable/operators/index.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { ifEmpty } from './if_empty';
|
||||
export { last } from './last';
|
||||
export { first } from './first';
|
||||
export { map } from './map';
|
||||
export { filter } from './filter';
|
||||
export { reduce } from './reduce';
|
||||
export { scan } from './scan';
|
||||
export { toArray } from './to_array';
|
||||
export { switchMap } from './switch_map';
|
||||
export { mergeMap } from './merge_map';
|
||||
export { skipRepeats } from './skip_repeats';
|
||||
export { toPromise } from './to_promise';
|
58
src/core/lib/kbn_observable/operators/last.ts
Normal file
58
src/core/lib/kbn_observable/operators/last.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { EmptyError } from '../errors';
|
||||
import { MonoTypeOperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Emits the last value emitted by the source Observable, then immediately
|
||||
* completes.
|
||||
*
|
||||
* @throws {EmptyError} Delivers an EmptyError to the Observer's `error`
|
||||
* callback if the Observable completes before any `next` notification was sent.
|
||||
*
|
||||
* @returns An Observable of the last item received.
|
||||
*/
|
||||
export function last<T>(): MonoTypeOperatorFunction<T> {
|
||||
return function lastOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let hasReceivedValue = false;
|
||||
let latest: T;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
hasReceivedValue = true;
|
||||
latest = value;
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
if (hasReceivedValue) {
|
||||
observer.next(latest);
|
||||
observer.complete();
|
||||
} else {
|
||||
observer.error(new EmptyError('last()'));
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
58
src/core/lib/kbn_observable/operators/map.ts
Normal file
58
src/core/lib/kbn_observable/operators/map.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Modifies each value from the source by passing it to `fn(item, i)` and
|
||||
* emitting the return value of that function instead.
|
||||
*
|
||||
* @param fn The function to apply to each `value` emitted by the source
|
||||
* Observable. The `index` parameter is the number `i` for the i-th emission
|
||||
* that has happened since the subscription, starting from the number `0`.
|
||||
* @return An Observable that emits the values from the source Observable
|
||||
* transformed by the given `fn` function.
|
||||
*/
|
||||
export function map<T, R>(fn: (value: T, index: number) => R): OperatorFunction<T, R> {
|
||||
return function mapOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let i = 0;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
let result: R;
|
||||
try {
|
||||
result = fn(value, i++);
|
||||
} catch (e) {
|
||||
observer.error(e);
|
||||
return;
|
||||
}
|
||||
observer.next(result);
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
118
src/core/lib/kbn_observable/operators/merge_map.ts
Normal file
118
src/core/lib/kbn_observable/operators/merge_map.ts
Normal file
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { $from } from '../factories';
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { Observable, ObservableInput } from '../observable';
|
||||
|
||||
/**
|
||||
* Projects each source value to an Observable which is merged in the output
|
||||
* Observable.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```js
|
||||
* const source = Observable.from([1, 2, 3]);
|
||||
* const observable = k$(source)(
|
||||
* mergeMap(x => Observable.of('a', x + 1))
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* Results in the following items emitted:
|
||||
* - a
|
||||
* - 2
|
||||
* - a
|
||||
* - 3
|
||||
* - a
|
||||
* - 4
|
||||
*
|
||||
* As you can see it merges the returned observable and emits every value from
|
||||
* that observable. You can think of it as being the same as `flatMap` on an
|
||||
* array, just that you return an Observable instead of an array.
|
||||
*
|
||||
* For more complex use-cases where you need the source variable for each value
|
||||
* in the newly created observable, an often used pattern is using `map` within
|
||||
* the `mergeMap`. E.g. let's say we want to return both the current value and
|
||||
* the newly created value:
|
||||
*
|
||||
* ```js
|
||||
* mergeMap(val =>
|
||||
* k$(someFn(val))(
|
||||
* map(newVal => ({ val, newVal })
|
||||
* )
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* Here you would go from having an observable of `val`s, to having an
|
||||
* observable of `{ val, newVal }` objects.
|
||||
*
|
||||
* @param project A function that, when applied to an item emitted by the source
|
||||
* Observable, returns an Observable.
|
||||
*/
|
||||
export function mergeMap<T, R>(
|
||||
project: (value: T, index: number) => ObservableInput<R>
|
||||
): OperatorFunction<T, R> {
|
||||
return function mergeMapOperation(source) {
|
||||
return new Observable(destination => {
|
||||
let completed = false;
|
||||
let active = 0;
|
||||
let i = 0;
|
||||
|
||||
source.subscribe({
|
||||
next(value) {
|
||||
let result;
|
||||
try {
|
||||
result = project(value, i++);
|
||||
} catch (error) {
|
||||
destination.error(error);
|
||||
return;
|
||||
}
|
||||
active++;
|
||||
|
||||
$from(result).subscribe({
|
||||
next(innerValue) {
|
||||
destination.next(innerValue);
|
||||
},
|
||||
error(err) {
|
||||
destination.error(err);
|
||||
},
|
||||
complete() {
|
||||
active--;
|
||||
|
||||
if (active === 0 && completed) {
|
||||
destination.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
error(err) {
|
||||
destination.error(err);
|
||||
},
|
||||
|
||||
complete() {
|
||||
completed = true;
|
||||
if (active === 0) {
|
||||
destination.complete();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
47
src/core/lib/kbn_observable/operators/reduce.ts
Normal file
47
src/core/lib/kbn_observable/operators/reduce.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { pipe } from '../lib';
|
||||
import { ifEmpty } from './if_empty';
|
||||
import { last } from './last';
|
||||
import { scan } from './scan';
|
||||
|
||||
/**
|
||||
* Applies the accumulator function to every value in the source observable and
|
||||
* emits the return value when the source completes.
|
||||
*
|
||||
* It's like {@link scan}, but only emits when the source observable completes,
|
||||
* not the current accumulation whenever the source emits a value.
|
||||
*
|
||||
* If no values are emitted, the `initialValue` will be emitted.
|
||||
*
|
||||
* @param accumulator The accumulator function called on each source value.
|
||||
* @param initialValue The initial accumulation value.
|
||||
* @return An Observable that emits a single value that is the result of
|
||||
* accumulating the values emitted by the source Observable.
|
||||
*/
|
||||
export function reduce<T, R>(
|
||||
accumulator: (acc: R, value: T, index: number) => R,
|
||||
initialValue: R
|
||||
): OperatorFunction<T, R> {
|
||||
return function reduceOperation(source) {
|
||||
return pipe(scan(accumulator, initialValue), ifEmpty(() => initialValue), last())(source);
|
||||
};
|
||||
}
|
64
src/core/lib/kbn_observable/operators/scan.ts
Normal file
64
src/core/lib/kbn_observable/operators/scan.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
/**
|
||||
* Applies the accumulator function to every value in the source stream and
|
||||
* emits the return value of each invocation.
|
||||
*
|
||||
* It's like {@link reduce}, but emits the current accumulation whenever the
|
||||
* source emits a value instead of emitting only when completed.
|
||||
*
|
||||
* @param accumulator The accumulator function called on each source value.
|
||||
* @param initialValue The initial accumulation value.
|
||||
* @return An observable of the accumulated values.
|
||||
*/
|
||||
export function scan<T, R>(
|
||||
accumulator: (acc: R, value: T, index: number) => R,
|
||||
initialValue: R
|
||||
): OperatorFunction<T, R> {
|
||||
return function scanOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let i = -1;
|
||||
let acc = initialValue;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
i += 1;
|
||||
|
||||
try {
|
||||
acc = accumulator(acc, value, i);
|
||||
|
||||
observer.next(acc);
|
||||
} catch (error) {
|
||||
observer.error(error);
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
68
src/core/lib/kbn_observable/operators/skip_repeats.ts
Normal file
68
src/core/lib/kbn_observable/operators/skip_repeats.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { MonoTypeOperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
const isStrictlyEqual = (a: any, b: any) => a === b;
|
||||
|
||||
/**
|
||||
* Returns an Observable that emits all items emitted by the source Observable
|
||||
* that are not equal to the previous item.
|
||||
*
|
||||
* @param [equals] Optional comparison function called to test if an item is
|
||||
* equal to the previous item in the source. Should return `true` if equal,
|
||||
* otherwise `false`. By default compares using reference equality, aka `===`.
|
||||
* @return An Observable that emits items from the source Observable with
|
||||
* distinct values.
|
||||
*/
|
||||
export function skipRepeats<T>(
|
||||
equals: (x: T, y: T) => boolean = isStrictlyEqual
|
||||
): MonoTypeOperatorFunction<T> {
|
||||
return function skipRepeatsOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let hasInitialValue = false;
|
||||
let currentValue: T;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
if (!hasInitialValue) {
|
||||
hasInitialValue = true;
|
||||
currentValue = value;
|
||||
observer.next(value);
|
||||
return;
|
||||
}
|
||||
|
||||
const isEqual = equals(currentValue, value);
|
||||
|
||||
if (!isEqual) {
|
||||
observer.next(value);
|
||||
currentValue = value;
|
||||
}
|
||||
},
|
||||
error(error) {
|
||||
observer.error(error);
|
||||
},
|
||||
complete() {
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
121
src/core/lib/kbn_observable/operators/switch_map.ts
Normal file
121
src/core/lib/kbn_observable/operators/switch_map.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { Observable, Subscription } from '../observable';
|
||||
|
||||
/**
|
||||
* Projects each source value to an Observable which is merged in the output
|
||||
* Observable, emitting values only from the most recently projected Observable.
|
||||
*
|
||||
* To understand how `switchMap` works, take a look at:
|
||||
* https://medium.com/@w.dave.w/becoming-more-reactive-with-rxjs-flatmap-and-switchmap-ccd3fb7b67fa
|
||||
*
|
||||
* It's kinda like a normal `flatMap`, except it's producing observables and you
|
||||
* _only_ care about the latest observable it produced. One use-case for
|
||||
* `switchMap` is if need to control what happens both when you create and when
|
||||
* you're done with an observable, like in the example below where we want to
|
||||
* write the pid file when we receive a pid config, and delete it when we
|
||||
* receive new config values (or when we stop the pid service).
|
||||
*
|
||||
* ```js
|
||||
* switchMap(config => {
|
||||
* return new Observable(() => {
|
||||
* const pid = new PidFile(config);
|
||||
* pid.writeFile();
|
||||
*
|
||||
* // Whenever a new observable is returned, `switchMap` will unsubscribe
|
||||
* // from the previous observable. That means that we can e.g. run teardown
|
||||
* // logic in the unsubscribe.
|
||||
* return function unsubscribe() {
|
||||
* pid.deleteFile();
|
||||
* };
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* Another example could be emitting a value X seconds after receiving it from
|
||||
* the source observable, but cancelling if another value is received before the
|
||||
* timeout, e.g.
|
||||
*
|
||||
* ```js
|
||||
* switchMap(value => {
|
||||
* return new Observable(observer => {
|
||||
* const id = setTimeout(() => {
|
||||
* observer.next(value);
|
||||
* }, 5000);
|
||||
*
|
||||
* return function unsubscribe() {
|
||||
* clearTimeout(id);
|
||||
* };
|
||||
* });
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function switchMap<T, R>(
|
||||
project: (value: T, index: number) => Observable<R>
|
||||
): OperatorFunction<T, R> {
|
||||
return function switchMapOperation(source) {
|
||||
return new Observable(observer => {
|
||||
let i = 0;
|
||||
let innerSubscription: Subscription | undefined;
|
||||
|
||||
return source.subscribe({
|
||||
next(value) {
|
||||
let result;
|
||||
try {
|
||||
result = project(value, i++);
|
||||
} catch (error) {
|
||||
observer.error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (innerSubscription !== undefined) {
|
||||
innerSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
innerSubscription = result.subscribe({
|
||||
next(innerVal) {
|
||||
observer.next(innerVal);
|
||||
},
|
||||
error(err) {
|
||||
observer.error(err);
|
||||
},
|
||||
});
|
||||
},
|
||||
error(err) {
|
||||
if (innerSubscription !== undefined) {
|
||||
innerSubscription.unsubscribe();
|
||||
innerSubscription = undefined;
|
||||
}
|
||||
|
||||
observer.error(err);
|
||||
},
|
||||
complete() {
|
||||
if (innerSubscription !== undefined) {
|
||||
innerSubscription.unsubscribe();
|
||||
innerSubscription = undefined;
|
||||
}
|
||||
|
||||
observer.complete();
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
36
src/core/lib/kbn_observable/operators/to_array.ts
Normal file
36
src/core/lib/kbn_observable/operators/to_array.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { OperatorFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
import { reduce } from './reduce';
|
||||
|
||||
function concat<T>(source: Observable<T>) {
|
||||
return reduce<T, T[]>((acc, item) => acc.concat([item]), [] as T[])(source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify a stream to produce a single array containing all of the items emitted
|
||||
* by source.
|
||||
*/
|
||||
export function toArray<T>(): OperatorFunction<T, T[]> {
|
||||
return function toArrayOperation(source) {
|
||||
return concat(source);
|
||||
};
|
||||
}
|
41
src/core/lib/kbn_observable/operators/to_promise.ts
Normal file
41
src/core/lib/kbn_observable/operators/to_promise.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { UnaryFunction } from '../interfaces';
|
||||
import { Observable } from '../observable';
|
||||
|
||||
export function toPromise<T>(): UnaryFunction<Observable<T>, Promise<T>> {
|
||||
return function toPromiseOperation(source) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let lastValue: T;
|
||||
|
||||
source.subscribe({
|
||||
next(value) {
|
||||
lastValue = value;
|
||||
},
|
||||
error(error) {
|
||||
reject(error);
|
||||
},
|
||||
complete() {
|
||||
resolve(lastValue);
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
114
src/core/lib/kbn_observable/subject.ts
Normal file
114
src/core/lib/kbn_observable/subject.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Observable, SubscriptionObserver } from './observable';
|
||||
|
||||
/**
|
||||
* A Subject is a special type of Observable that allows values to be
|
||||
* multicasted to many Observers. While plain Observables are unicast (each
|
||||
* subscribed Observer owns an independent execution of the Observable),
|
||||
* Subjects are multicast.
|
||||
*
|
||||
* Every Subject is an Observable. Given a Subject, you can subscribe to it in
|
||||
* the same way you subscribe to any Observable, and you will start receiving
|
||||
* values normally. From the perspective of the Observer, it cannot tell whether
|
||||
* the Observable execution is coming from a plain unicast Observable or a
|
||||
* Subject.
|
||||
*
|
||||
* Internally to the Subject, `subscribe` does not invoke a new execution that
|
||||
* delivers values. It simply registers the given Observer in a list of
|
||||
* Observers, similarly to how `addListener` usually works in other libraries
|
||||
* and languages.
|
||||
*
|
||||
* Every Subject is an Observer. It is an object with the methods `next(v)`,
|
||||
* `error(e)`, and `complete()`. To feed a new value to the Subject, just call
|
||||
* `next(theValue)`, and it will be multicasted to the Observers registered to
|
||||
* listen to the Subject.
|
||||
*
|
||||
* Learn more about Subjects:
|
||||
* - http://reactivex.io/documentation/subject.html
|
||||
* - http://davesexton.com/blog/post/To-Use-Subject-Or-Not-To-Use-Subject.aspx
|
||||
*/
|
||||
export class Subject<T> extends Observable<T> {
|
||||
protected observers: Set<SubscriptionObserver<T>> = new Set();
|
||||
protected isStopped = false;
|
||||
protected thrownError?: Error;
|
||||
|
||||
constructor() {
|
||||
super(observer => this.registerObserver(observer));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value The value that will be forwarded to every observer subscribed
|
||||
* to this subject.
|
||||
*/
|
||||
public next(value: T) {
|
||||
for (const observer of this.observers) {
|
||||
observer.next(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param error The error that will be forwarded to every observer subscribed
|
||||
* to this subject.
|
||||
*/
|
||||
public error(error: Error) {
|
||||
this.thrownError = error;
|
||||
this.isStopped = true;
|
||||
|
||||
for (const observer of this.observers) {
|
||||
observer.error(error);
|
||||
}
|
||||
|
||||
this.observers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes all the subscribed observers, then clears the list of observers.
|
||||
*/
|
||||
public complete() {
|
||||
this.isStopped = true;
|
||||
|
||||
for (const observer of this.observers) {
|
||||
observer.complete();
|
||||
}
|
||||
|
||||
this.observers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable, so the observer methods are hidden.
|
||||
*/
|
||||
public asObservable(): Observable<T> {
|
||||
return new Observable(observer => this.subscribe(observer));
|
||||
}
|
||||
|
||||
protected registerObserver(observer: SubscriptionObserver<T>) {
|
||||
if (this.isStopped) {
|
||||
if (this.thrownError !== undefined) {
|
||||
observer.error(this.thrownError);
|
||||
} else {
|
||||
observer.complete();
|
||||
}
|
||||
} else {
|
||||
this.observers.add(observer);
|
||||
return () => this.observers.delete(observer);
|
||||
}
|
||||
}
|
||||
}
|
6
src/core/server/README.md
Normal file
6
src/core/server/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
Platform Server Modules
|
||||
=======================
|
||||
|
||||
Http Server
|
||||
-----------
|
||||
TODO: explain
|
3
src/core/server/config/__tests__/__fixtures__/config.yml
Normal file
3
src/core/server/config/__tests__/__fixtures__/config.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
pid:
|
||||
enabled: true
|
||||
file: '/var/run/kibana.pid'
|
|
@ -0,0 +1,2 @@
|
|||
pid.enabled: true
|
||||
pid.file: '/var/run/kibana.pid'
|
53
src/core/server/config/__tests__/__mocks__/env.ts
Normal file
53
src/core/server/config/__tests__/__mocks__/env.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
// Test helpers to simplify mocking environment options.
|
||||
|
||||
import { EnvOptions } from '../../env';
|
||||
|
||||
interface MockEnvOptions {
|
||||
config?: string;
|
||||
kbnServer?: any;
|
||||
mode?: EnvOptions['mode']['name'];
|
||||
packageInfo?: Partial<EnvOptions['packageInfo']>;
|
||||
}
|
||||
|
||||
export function getEnvOptions({
|
||||
config,
|
||||
kbnServer,
|
||||
mode = 'development',
|
||||
packageInfo = {},
|
||||
}: MockEnvOptions = {}): EnvOptions {
|
||||
return {
|
||||
config,
|
||||
kbnServer,
|
||||
mode: {
|
||||
dev: mode === 'development',
|
||||
name: mode,
|
||||
prod: mode === 'production',
|
||||
},
|
||||
packageInfo: {
|
||||
branch: 'some-branch',
|
||||
buildNum: 1,
|
||||
buildSha: 'some-sha-256',
|
||||
version: 'some-version',
|
||||
...packageInfo,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`correctly passes context 1`] = `
|
||||
ExampleClassWithSchema {
|
||||
"value": Object {
|
||||
"branchRef": "feature-v1",
|
||||
"buildNumRef": 100,
|
||||
"buildShaRef": "feature-v1-build-sha",
|
||||
"devRef": true,
|
||||
"prodRef": false,
|
||||
"versionRef": "v1",
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`throws error if config class does not implement 'schema' 1`] = `[Error: The config class [ExampleClass] did not contain a static 'schema' field, which is required when creating a config instance]`;
|
||||
|
||||
exports[`throws if config at path does not match schema 1`] = `"[key]: expected value of type [string] but got [number]"`;
|
79
src/core/server/config/__tests__/apply_argv.test.ts
Normal file
79
src/core/server/config/__tests__/apply_argv.test.ts
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { ObjectToRawConfigAdapter, RawConfig } from '..';
|
||||
|
||||
/**
|
||||
* Overrides some config values with ones from argv.
|
||||
*
|
||||
* @param config `RawConfig` instance to update config values for.
|
||||
* @param argv Argv object with key/value pairs.
|
||||
*/
|
||||
export function overrideConfigWithArgv(config: RawConfig, argv: { [key: string]: any }) {
|
||||
if (argv.port != null) {
|
||||
config.set(['server', 'port'], argv.port);
|
||||
}
|
||||
|
||||
if (argv.host != null) {
|
||||
config.set(['server', 'host'], argv.host);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
test('port', () => {
|
||||
const argv = {
|
||||
port: 123,
|
||||
};
|
||||
|
||||
const config = new ObjectToRawConfigAdapter({
|
||||
server: { port: 456 },
|
||||
});
|
||||
|
||||
overrideConfigWithArgv(config, argv);
|
||||
|
||||
expect(config.get('server.port')).toEqual(123);
|
||||
});
|
||||
|
||||
test('host', () => {
|
||||
const argv = {
|
||||
host: 'example.org',
|
||||
};
|
||||
|
||||
const config = new ObjectToRawConfigAdapter({
|
||||
server: { host: 'org.example' },
|
||||
});
|
||||
|
||||
overrideConfigWithArgv(config, argv);
|
||||
|
||||
expect(config.get('server.host')).toEqual('example.org');
|
||||
});
|
||||
|
||||
test('ignores unknown', () => {
|
||||
const argv = {
|
||||
unknown: 'some value',
|
||||
};
|
||||
|
||||
const config = new ObjectToRawConfigAdapter({});
|
||||
jest.spyOn(config, 'set');
|
||||
|
||||
overrideConfigWithArgv(config, argv);
|
||||
|
||||
expect(config.set).not.toHaveBeenCalled();
|
||||
});
|
283
src/core/server/config/__tests__/config_service.test.ts
Normal file
283
src/core/server/config/__tests__/config_service.test.ts
Normal file
|
@ -0,0 +1,283 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
/* tslint:disable max-classes-per-file */
|
||||
import { BehaviorSubject, first, k$, toPromise } from '../../../lib/kbn_observable';
|
||||
import { AnyType, schema, TypeOf } from '../schema';
|
||||
|
||||
import { ConfigService, ObjectToRawConfigAdapter } from '..';
|
||||
import { logger } from '../../logging/__mocks__';
|
||||
import { Env } from '../env';
|
||||
import { getEnvOptions } from './__mocks__/env';
|
||||
|
||||
const emptyArgv = getEnvOptions();
|
||||
const defaultEnv = new Env('/kibana', emptyArgv);
|
||||
|
||||
test('returns config at path as observable', async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'foo' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const configs = configService.atPath('key', ExampleClassWithStringSchema);
|
||||
const exampleConfig = await k$(configs)(first(), toPromise());
|
||||
|
||||
expect(exampleConfig.value).toBe('foo');
|
||||
});
|
||||
|
||||
test('throws if config at path does not match schema', async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 123 }));
|
||||
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
const configs = configService.atPath('key', ExampleClassWithStringSchema);
|
||||
|
||||
try {
|
||||
await k$(configs)(first(), toPromise());
|
||||
} catch (e) {
|
||||
expect(e.message).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
test("returns undefined if fetching optional config at a path that doesn't exist", async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: 'bar' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const configs = configService.optionalAtPath('unique-name', ExampleClassWithStringSchema);
|
||||
const exampleConfig = await k$(configs)(first(), toPromise());
|
||||
|
||||
expect(exampleConfig).toBeUndefined();
|
||||
});
|
||||
|
||||
test('returns observable config at optional path if it exists', async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ value: 'bar' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const configs = configService.optionalAtPath('value', ExampleClassWithStringSchema);
|
||||
const exampleConfig: any = await k$(configs)(first(), toPromise());
|
||||
|
||||
expect(exampleConfig).toBeDefined();
|
||||
expect(exampleConfig.value).toBe('bar');
|
||||
});
|
||||
|
||||
test("does not push new configs when reloading if config at path hasn't changed", async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'value' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const valuesReceived: any[] = [];
|
||||
configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => {
|
||||
valuesReceived.push(config.value);
|
||||
});
|
||||
|
||||
config$.next(new ObjectToRawConfigAdapter({ key: 'value' }));
|
||||
|
||||
expect(valuesReceived).toEqual(['value']);
|
||||
});
|
||||
|
||||
test('pushes new config when reloading and config at path has changed', async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'value' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const valuesReceived: any[] = [];
|
||||
configService.atPath('key', ExampleClassWithStringSchema).subscribe(config => {
|
||||
valuesReceived.push(config.value);
|
||||
});
|
||||
|
||||
config$.next(new ObjectToRawConfigAdapter({ key: 'new value' }));
|
||||
|
||||
expect(valuesReceived).toEqual(['value', 'new value']);
|
||||
});
|
||||
|
||||
test("throws error if config class does not implement 'schema'", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
class ExampleClass {}
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ key: 'value' }));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const configs = configService.atPath('key', ExampleClass as any);
|
||||
|
||||
try {
|
||||
await k$(configs)(first(), toPromise());
|
||||
} catch (e) {
|
||||
expect(e).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
test('tracks unhandled paths', async () => {
|
||||
const initialConfig = {
|
||||
bar: {
|
||||
deep1: {
|
||||
key: '123',
|
||||
},
|
||||
deep2: {
|
||||
key: '321',
|
||||
},
|
||||
},
|
||||
foo: 'value',
|
||||
quux: {
|
||||
deep1: {
|
||||
key: 'hello',
|
||||
},
|
||||
deep2: {
|
||||
key: 'world',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
configService.atPath('foo', createClassWithSchema(schema.string()));
|
||||
configService.atPath(
|
||||
['bar', 'deep2'],
|
||||
createClassWithSchema(
|
||||
schema.object({
|
||||
key: schema.string(),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const unused = await configService.getUnusedPaths();
|
||||
|
||||
expect(unused).toEqual(['bar.deep1.key', 'quux.deep1.key', 'quux.deep2.key']);
|
||||
});
|
||||
|
||||
test('correctly passes context', async () => {
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter({ foo: {} }));
|
||||
|
||||
const env = new Env(
|
||||
'/kibana',
|
||||
getEnvOptions({
|
||||
mode: 'development',
|
||||
packageInfo: {
|
||||
branch: 'feature-v1',
|
||||
buildNum: 100,
|
||||
buildSha: 'feature-v1-build-sha',
|
||||
version: 'v1',
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const configService = new ConfigService(config$, env, logger);
|
||||
const configs = configService.atPath(
|
||||
'foo',
|
||||
createClassWithSchema(
|
||||
schema.object({
|
||||
branchRef: schema.string({
|
||||
defaultValue: schema.contextRef('branch'),
|
||||
}),
|
||||
buildNumRef: schema.number({
|
||||
defaultValue: schema.contextRef('buildNum'),
|
||||
}),
|
||||
buildShaRef: schema.string({
|
||||
defaultValue: schema.contextRef('buildSha'),
|
||||
}),
|
||||
devRef: schema.boolean({ defaultValue: schema.contextRef('dev') }),
|
||||
prodRef: schema.boolean({ defaultValue: schema.contextRef('prod') }),
|
||||
versionRef: schema.string({
|
||||
defaultValue: schema.contextRef('version'),
|
||||
}),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
expect(await k$(configs)(first(), toPromise())).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handles enabled path, but only marks the enabled path as used', async () => {
|
||||
const initialConfig = {
|
||||
pid: {
|
||||
enabled: true,
|
||||
file: '/some/file.pid',
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const isEnabled = await configService.isEnabledAtPath('pid');
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
const unusedPaths = await configService.getUnusedPaths();
|
||||
expect(unusedPaths).toEqual(['pid.file']);
|
||||
});
|
||||
|
||||
test('handles enabled path when path is array', async () => {
|
||||
const initialConfig = {
|
||||
pid: {
|
||||
enabled: true,
|
||||
file: '/some/file.pid',
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const isEnabled = await configService.isEnabledAtPath(['pid']);
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
const unusedPaths = await configService.getUnusedPaths();
|
||||
expect(unusedPaths).toEqual(['pid.file']);
|
||||
});
|
||||
|
||||
test('handles disabled path and marks config as used', async () => {
|
||||
const initialConfig = {
|
||||
pid: {
|
||||
enabled: false,
|
||||
file: '/some/file.pid',
|
||||
},
|
||||
};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const isEnabled = await configService.isEnabledAtPath('pid');
|
||||
expect(isEnabled).toBe(false);
|
||||
|
||||
const unusedPaths = await configService.getUnusedPaths();
|
||||
expect(unusedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
test('treats config as enabled if config path is not present in config', async () => {
|
||||
const initialConfig = {};
|
||||
|
||||
const config$ = new BehaviorSubject(new ObjectToRawConfigAdapter(initialConfig));
|
||||
const configService = new ConfigService(config$, defaultEnv, logger);
|
||||
|
||||
const isEnabled = await configService.isEnabledAtPath('pid');
|
||||
expect(isEnabled).toBe(true);
|
||||
|
||||
const unusedPaths = await configService.getUnusedPaths();
|
||||
expect(unusedPaths).toEqual([]);
|
||||
});
|
||||
|
||||
function createClassWithSchema(s: AnyType) {
|
||||
return class ExampleClassWithSchema {
|
||||
public static schema = s;
|
||||
|
||||
constructor(readonly value: TypeOf<typeof s>) {}
|
||||
};
|
||||
}
|
||||
|
||||
class ExampleClassWithStringSchema {
|
||||
public static schema = schema.string();
|
||||
|
||||
constructor(readonly value: string) {}
|
||||
}
|
156
src/core/server/config/__tests__/ensure_deep_object.test.ts
Normal file
156
src/core/server/config/__tests__/ensure_deep_object.test.ts
Normal file
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { ensureDeepObject } from '../ensure_deep_object';
|
||||
|
||||
test('flat object', () => {
|
||||
const obj = {
|
||||
'foo.a': 1,
|
||||
'foo.b': 2,
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
foo: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('deep object', () => {
|
||||
const obj = {
|
||||
foo: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
foo: {
|
||||
a: 1,
|
||||
b: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('flat within deep object', () => {
|
||||
const obj = {
|
||||
foo: {
|
||||
b: 2,
|
||||
'bar.a': 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
foo: {
|
||||
b: 2,
|
||||
bar: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('flat then flat object', () => {
|
||||
const obj = {
|
||||
'foo.bar': {
|
||||
b: 2,
|
||||
'quux.a': 1,
|
||||
},
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
foo: {
|
||||
bar: {
|
||||
b: 2,
|
||||
quux: {
|
||||
a: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('full with empty array', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: [],
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
a: 1,
|
||||
b: [],
|
||||
});
|
||||
});
|
||||
|
||||
test('full with array of primitive values', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: [1, 2, 3],
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
a: 1,
|
||||
b: [1, 2, 3],
|
||||
});
|
||||
});
|
||||
|
||||
test('full with array of full objects', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: [{ c: 2 }, { d: 3 }],
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
a: 1,
|
||||
b: [{ c: 2 }, { d: 3 }],
|
||||
});
|
||||
});
|
||||
|
||||
test('full with array of flat objects', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
b: [{ 'c.d': 2 }, { 'e.f': 3 }],
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
a: 1,
|
||||
b: [{ c: { d: 2 } }, { e: { f: 3 } }],
|
||||
});
|
||||
});
|
||||
|
||||
test('flat with flat and array of flat objects', () => {
|
||||
const obj = {
|
||||
a: 1,
|
||||
'b.c': 2,
|
||||
d: [3, { 'e.f': 4 }, { 'g.h': 5 }],
|
||||
};
|
||||
|
||||
expect(ensureDeepObject(obj)).toEqual({
|
||||
a: 1,
|
||||
b: { c: 2 },
|
||||
d: [3, { e: { f: 4 } }, { g: { h: 5 } }],
|
||||
});
|
||||
});
|
||||
|
||||
test('array composed of flat objects', () => {
|
||||
const arr = [{ 'c.d': 2 }, { 'e.f': 3 }];
|
||||
|
||||
expect(ensureDeepObject(arr)).toEqual([{ c: { d: 2 } }, { e: { f: 3 } }]);
|
||||
});
|
104
src/core/server/config/__tests__/env.test.ts
Normal file
104
src/core/server/config/__tests__/env.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
jest.mock('process', () => ({
|
||||
cwd() {
|
||||
return '/test/cwd';
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('path', () => ({
|
||||
resolve(...pathSegments: string[]) {
|
||||
return pathSegments.join('/');
|
||||
},
|
||||
}));
|
||||
|
||||
import { Env } from '../env';
|
||||
import { getEnvOptions } from './__mocks__/env';
|
||||
|
||||
test('correctly creates default environment with empty options.', () => {
|
||||
const envOptions = getEnvOptions();
|
||||
const defaultEnv = Env.createDefault(envOptions);
|
||||
|
||||
expect(defaultEnv.homeDir).toEqual('/test/cwd');
|
||||
expect(defaultEnv.configDir).toEqual('/test/cwd/config');
|
||||
expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins');
|
||||
expect(defaultEnv.binDir).toEqual('/test/cwd/bin');
|
||||
expect(defaultEnv.logDir).toEqual('/test/cwd/log');
|
||||
expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui');
|
||||
|
||||
expect(defaultEnv.getConfigFile()).toEqual('/test/cwd/config/kibana.yml');
|
||||
expect(defaultEnv.getLegacyKbnServer()).toBeUndefined();
|
||||
expect(defaultEnv.getMode()).toEqual(envOptions.mode);
|
||||
expect(defaultEnv.getPackageInfo()).toEqual(envOptions.packageInfo);
|
||||
});
|
||||
|
||||
test('correctly creates default environment with options overrides.', () => {
|
||||
const mockEnvOptions = getEnvOptions({
|
||||
config: '/some/other/path/some-kibana.yml',
|
||||
kbnServer: {},
|
||||
mode: 'production',
|
||||
packageInfo: {
|
||||
branch: 'feature-v1',
|
||||
buildNum: 100,
|
||||
buildSha: 'feature-v1-build-sha',
|
||||
version: 'v1',
|
||||
},
|
||||
});
|
||||
const defaultEnv = Env.createDefault(mockEnvOptions);
|
||||
|
||||
expect(defaultEnv.homeDir).toEqual('/test/cwd');
|
||||
expect(defaultEnv.configDir).toEqual('/test/cwd/config');
|
||||
expect(defaultEnv.corePluginsDir).toEqual('/test/cwd/core_plugins');
|
||||
expect(defaultEnv.binDir).toEqual('/test/cwd/bin');
|
||||
expect(defaultEnv.logDir).toEqual('/test/cwd/log');
|
||||
expect(defaultEnv.staticFilesDir).toEqual('/test/cwd/ui');
|
||||
|
||||
expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config);
|
||||
expect(defaultEnv.getLegacyKbnServer()).toBe(mockEnvOptions.kbnServer);
|
||||
expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode);
|
||||
expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo);
|
||||
});
|
||||
|
||||
test('correctly creates environment with constructor.', () => {
|
||||
const mockEnvOptions = getEnvOptions({
|
||||
config: '/some/other/path/some-kibana.yml',
|
||||
mode: 'production',
|
||||
packageInfo: {
|
||||
branch: 'feature-v1',
|
||||
buildNum: 100,
|
||||
buildSha: 'feature-v1-build-sha',
|
||||
version: 'v1',
|
||||
},
|
||||
});
|
||||
|
||||
const defaultEnv = new Env('/some/home/dir', mockEnvOptions);
|
||||
|
||||
expect(defaultEnv.homeDir).toEqual('/some/home/dir');
|
||||
expect(defaultEnv.configDir).toEqual('/some/home/dir/config');
|
||||
expect(defaultEnv.corePluginsDir).toEqual('/some/home/dir/core_plugins');
|
||||
expect(defaultEnv.binDir).toEqual('/some/home/dir/bin');
|
||||
expect(defaultEnv.logDir).toEqual('/some/home/dir/log');
|
||||
expect(defaultEnv.staticFilesDir).toEqual('/some/home/dir/ui');
|
||||
|
||||
expect(defaultEnv.getConfigFile()).toEqual(mockEnvOptions.config);
|
||||
expect(defaultEnv.getLegacyKbnServer()).toBeUndefined();
|
||||
expect(defaultEnv.getMode()).toEqual(mockEnvOptions.mode);
|
||||
expect(defaultEnv.getPackageInfo()).toEqual(mockEnvOptions.packageInfo);
|
||||
});
|
132
src/core/server/config/__tests__/raw_config_service.test.ts
Normal file
132
src/core/server/config/__tests__/raw_config_service.test.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
const mockGetConfigFromFile = jest.fn();
|
||||
|
||||
jest.mock('../read_config', () => ({
|
||||
getConfigFromFile: mockGetConfigFromFile,
|
||||
}));
|
||||
|
||||
import { first, k$, toPromise } from '../../../lib/kbn_observable';
|
||||
import { RawConfigService } from '../raw_config_service';
|
||||
|
||||
const configFile = '/config/kibana.yml';
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetConfigFromFile.mockReset();
|
||||
mockGetConfigFromFile.mockImplementation(() => ({}));
|
||||
});
|
||||
|
||||
test('loads raw config when started', () => {
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
expect(mockGetConfigFromFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetConfigFromFile).toHaveBeenLastCalledWith(configFile);
|
||||
});
|
||||
|
||||
test('re-reads the config when reloading', () => {
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
mockGetConfigFromFile.mockClear();
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ foo: 'bar' }));
|
||||
|
||||
configService.reloadConfig();
|
||||
|
||||
expect(mockGetConfigFromFile).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetConfigFromFile).toHaveBeenLastCalledWith(configFile);
|
||||
});
|
||||
|
||||
test('returns config at path as observable', async () => {
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
|
||||
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
const exampleConfig = await k$(configService.getConfig$())(first(), toPromise());
|
||||
|
||||
expect(exampleConfig.get('key')).toEqual('value');
|
||||
expect(exampleConfig.getFlattenedPaths()).toEqual(['key']);
|
||||
});
|
||||
|
||||
test("does not push new configs when reloading if config at path hasn't changed", async () => {
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
|
||||
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
const valuesReceived: any[] = [];
|
||||
configService.getConfig$().subscribe(config => {
|
||||
valuesReceived.push(config);
|
||||
});
|
||||
|
||||
mockGetConfigFromFile.mockClear();
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
|
||||
|
||||
configService.reloadConfig();
|
||||
|
||||
expect(valuesReceived).toHaveLength(1);
|
||||
expect(valuesReceived[0].get('key')).toEqual('value');
|
||||
expect(valuesReceived[0].getFlattenedPaths()).toEqual(['key']);
|
||||
});
|
||||
|
||||
test('pushes new config when reloading and config at path has changed', async () => {
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
|
||||
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
const valuesReceived: any[] = [];
|
||||
configService.getConfig$().subscribe(config => {
|
||||
valuesReceived.push(config);
|
||||
});
|
||||
|
||||
mockGetConfigFromFile.mockClear();
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'new value' }));
|
||||
|
||||
configService.reloadConfig();
|
||||
|
||||
expect(valuesReceived).toHaveLength(2);
|
||||
expect(valuesReceived[0].get('key')).toEqual('value');
|
||||
expect(valuesReceived[0].getFlattenedPaths()).toEqual(['key']);
|
||||
expect(valuesReceived[1].get('key')).toEqual('new value');
|
||||
expect(valuesReceived[1].getFlattenedPaths()).toEqual(['key']);
|
||||
});
|
||||
|
||||
test('completes config observables when stopped', done => {
|
||||
expect.assertions(0);
|
||||
|
||||
mockGetConfigFromFile.mockImplementation(() => ({ key: 'value' }));
|
||||
|
||||
const configService = new RawConfigService(configFile);
|
||||
|
||||
configService.loadConfig();
|
||||
|
||||
configService.getConfig$().subscribe({
|
||||
complete: () => done(),
|
||||
});
|
||||
|
||||
configService.stop();
|
||||
});
|
44
src/core/server/config/__tests__/read_config.test.ts
Normal file
44
src/core/server/config/__tests__/read_config.test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { getConfigFromFile } from '../read_config';
|
||||
|
||||
const fixtureFile = (name: string) => `${__dirname}/__fixtures__/${name}`;
|
||||
|
||||
test('reads yaml from file system and parses to json', () => {
|
||||
const config = getConfigFromFile(fixtureFile('config.yml'));
|
||||
|
||||
expect(config).toEqual({
|
||||
pid: {
|
||||
enabled: true,
|
||||
file: '/var/run/kibana.pid',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns a deep object', () => {
|
||||
const config = getConfigFromFile(fixtureFile('/config_flat.yml'));
|
||||
|
||||
expect(config).toEqual({
|
||||
pid: {
|
||||
enabled: true,
|
||||
file: '/var/run/kibana.pid',
|
||||
},
|
||||
});
|
||||
});
|
179
src/core/server/config/config_service.ts
Normal file
179
src/core/server/config/config_service.ts
Normal file
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { isEqual } from 'lodash';
|
||||
import { first, k$, map, Observable, skipRepeats, toPromise } from '../../lib/kbn_observable';
|
||||
|
||||
import { Logger, LoggerFactory } from '../logging';
|
||||
import { ConfigWithSchema } from './config_with_schema';
|
||||
import { Env } from './env';
|
||||
import { RawConfig } from './raw_config';
|
||||
import { AnyType } from './schema';
|
||||
|
||||
export type ConfigPath = string | string[];
|
||||
|
||||
export class ConfigService {
|
||||
private readonly log: Logger;
|
||||
|
||||
/**
|
||||
* Whenever a config if read at a path, we mark that path as 'handled'. We can
|
||||
* then list all unhandled config paths when the startup process is completed.
|
||||
*/
|
||||
private readonly handledPaths: ConfigPath[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly config$: Observable<RawConfig>,
|
||||
readonly env: Env,
|
||||
logger: LoggerFactory
|
||||
) {
|
||||
this.log = logger.get('config');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full config object observable. This is not intended for
|
||||
* "normal use", but for features that _need_ access to the full object.
|
||||
*/
|
||||
public getConfig$() {
|
||||
return this.config$;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the subset of the config at the specified `path` and validates it
|
||||
* against the static `schema` on the given `ConfigClass`.
|
||||
*
|
||||
* @param path The path to the desired subset of the config.
|
||||
* @param ConfigClass A class (not an instance of a class) that contains a
|
||||
* static `schema` that we validate the config at the given `path` against.
|
||||
*/
|
||||
public atPath<Schema extends AnyType, Config>(
|
||||
path: ConfigPath,
|
||||
ConfigClass: ConfigWithSchema<Schema, Config>
|
||||
) {
|
||||
return k$(this.getDistinctRawConfig(path))(
|
||||
map(rawConfig => this.createConfig(path, rawConfig, ConfigClass))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as `atPath`, but returns `undefined` if there is no config at the
|
||||
* specified path.
|
||||
*
|
||||
* @see atPath
|
||||
*/
|
||||
public optionalAtPath<Schema extends AnyType, Config>(
|
||||
path: ConfigPath,
|
||||
ConfigClass: ConfigWithSchema<Schema, Config>
|
||||
) {
|
||||
return k$(this.getDistinctRawConfig(path))(
|
||||
map(
|
||||
rawConfig =>
|
||||
rawConfig === undefined ? undefined : this.createConfig(path, rawConfig, ConfigClass)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async isEnabledAtPath(path: ConfigPath) {
|
||||
const enabledPath = createPluginEnabledPath(path);
|
||||
|
||||
const config = await k$(this.config$)(first(), toPromise());
|
||||
|
||||
if (!config.has(enabledPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isEnabled = config.get(enabledPath);
|
||||
|
||||
if (isEnabled === false) {
|
||||
// If the plugin is _not_ enabled, we mark the entire plugin path as
|
||||
// handled, as it's expected that it won't be used.
|
||||
this.markAsHandled(path);
|
||||
return false;
|
||||
}
|
||||
|
||||
// If plugin enabled we mark the enabled path as handled, as we for example
|
||||
// can have plugins that don't have _any_ config except for this field, and
|
||||
// therefore have no reason to try to get the config.
|
||||
this.markAsHandled(enabledPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async getUnusedPaths(): Promise<string[]> {
|
||||
const config = await k$(this.config$)(first(), toPromise());
|
||||
const handledPaths = this.handledPaths.map(pathToString);
|
||||
|
||||
return config.getFlattenedPaths().filter(path => !isPathHandled(path, handledPaths));
|
||||
}
|
||||
|
||||
private createConfig<Schema extends AnyType, Config>(
|
||||
path: ConfigPath,
|
||||
rawConfig: {},
|
||||
ConfigClass: ConfigWithSchema<Schema, Config>
|
||||
) {
|
||||
const namespace = Array.isArray(path) ? path.join('.') : path;
|
||||
|
||||
const configSchema = ConfigClass.schema;
|
||||
|
||||
if (configSchema === undefined || typeof configSchema.validate !== 'function') {
|
||||
throw new Error(
|
||||
`The config class [${
|
||||
ConfigClass.name
|
||||
}] did not contain a static 'schema' field, which is required when creating a config instance`
|
||||
);
|
||||
}
|
||||
|
||||
const environmentMode = this.env.getMode();
|
||||
const config = ConfigClass.schema.validate(
|
||||
rawConfig,
|
||||
{
|
||||
dev: environmentMode.dev,
|
||||
prod: environmentMode.prod,
|
||||
...this.env.getPackageInfo(),
|
||||
},
|
||||
namespace
|
||||
);
|
||||
return new ConfigClass(config, this.env);
|
||||
}
|
||||
|
||||
private getDistinctRawConfig(path: ConfigPath) {
|
||||
this.markAsHandled(path);
|
||||
|
||||
return k$(this.config$)(map(config => config.get(path)), skipRepeats(isEqual));
|
||||
}
|
||||
|
||||
private markAsHandled(path: ConfigPath) {
|
||||
this.log.debug(`Marking config path as handled: ${path}`);
|
||||
this.handledPaths.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
const createPluginEnabledPath = (configPath: string | string[]) => {
|
||||
if (Array.isArray(configPath)) {
|
||||
return configPath.concat('enabled');
|
||||
}
|
||||
return `${configPath}.enabled`;
|
||||
};
|
||||
|
||||
const pathToString = (path: ConfigPath) => (Array.isArray(path) ? path.join('.') : path);
|
||||
|
||||
/**
|
||||
* A path is considered 'handled' if it is a subset of any of the already
|
||||
* handled paths.
|
||||
*/
|
||||
const isPathHandled = (path: string, handledPaths: string[]) =>
|
||||
handledPaths.some(handledPath => path.startsWith(handledPath));
|
47
src/core/server/config/config_with_schema.ts
Normal file
47
src/core/server/config/config_with_schema.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
// TODO inline all of these
|
||||
import { Env } from './env';
|
||||
import { AnyType, TypeOf } from './schema';
|
||||
|
||||
/**
|
||||
* Interface that defines the static side of a config class.
|
||||
*
|
||||
* (Remember that a class has two types: the type of the static side and the
|
||||
* type of the instance side, see https://www.typescriptlang.org/docs/handbook/interfaces.html#difference-between-the-static-and-instance-sides-of-classes)
|
||||
*
|
||||
* This can't be used to define the config class because of how interfaces work
|
||||
* in TypeScript, but it can be used to ensure we have a config class that
|
||||
* matches whenever it's used.
|
||||
*/
|
||||
export interface ConfigWithSchema<S extends AnyType, Config> {
|
||||
/**
|
||||
* Any config class must define a schema that validates the config, based on
|
||||
* the injected `schema` helper.
|
||||
*/
|
||||
schema: S;
|
||||
|
||||
/**
|
||||
* @param validatedConfig The result of validating the static `schema` above.
|
||||
* @param env An instance of the `Env` class that defines environment specific
|
||||
* variables.
|
||||
*/
|
||||
new (validatedConfig: TypeOf<S>, env: Env): Config;
|
||||
}
|
64
src/core/server/config/ensure_deep_object.ts
Normal file
64
src/core/server/config/ensure_deep_object.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
const separator = '.';
|
||||
|
||||
/**
|
||||
* Recursively traverses through the object's properties and expands ones with
|
||||
* dot-separated names into nested objects (eg. { a.b: 'c'} -> { a: { b: 'c' }).
|
||||
* @param obj Object to traverse through.
|
||||
* @returns Same object instance with expanded properties.
|
||||
*/
|
||||
export function ensureDeepObject(obj: any): any {
|
||||
if (obj == null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => ensureDeepObject(item));
|
||||
}
|
||||
|
||||
return Object.keys(obj).reduce(
|
||||
(fullObject, propertyKey) => {
|
||||
const propertyValue = obj[propertyKey];
|
||||
if (!propertyKey.includes(separator)) {
|
||||
fullObject[propertyKey] = ensureDeepObject(propertyValue);
|
||||
} else {
|
||||
walk(fullObject, propertyKey.split(separator), propertyValue);
|
||||
}
|
||||
|
||||
return fullObject;
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
}
|
||||
|
||||
function walk(obj: any, keys: string[], value: any) {
|
||||
const key = keys.shift()!;
|
||||
if (keys.length === 0) {
|
||||
obj[key] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
if (obj[key] === undefined) {
|
||||
obj[key] = {};
|
||||
}
|
||||
|
||||
walk(obj[key], keys, ensureDeepObject(value));
|
||||
}
|
100
src/core/server/config/env.ts
Normal file
100
src/core/server/config/env.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import process from 'process';
|
||||
|
||||
import { LegacyKbnServer } from '../legacy_compat';
|
||||
|
||||
interface PackageInfo {
|
||||
version: string;
|
||||
branch: string;
|
||||
buildNum: number;
|
||||
buildSha: string;
|
||||
}
|
||||
|
||||
interface EnvironmentMode {
|
||||
name: 'development' | 'production';
|
||||
dev: boolean;
|
||||
prod: boolean;
|
||||
}
|
||||
|
||||
export interface EnvOptions {
|
||||
config?: string;
|
||||
kbnServer?: any;
|
||||
packageInfo: PackageInfo;
|
||||
mode: EnvironmentMode;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class Env {
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static createDefault(options: EnvOptions): Env {
|
||||
return new Env(process.cwd(), options);
|
||||
}
|
||||
|
||||
public readonly configDir: string;
|
||||
public readonly corePluginsDir: string;
|
||||
public readonly binDir: string;
|
||||
public readonly logDir: string;
|
||||
public readonly staticFilesDir: string;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
constructor(readonly homeDir: string, private readonly options: EnvOptions) {
|
||||
this.configDir = resolve(this.homeDir, 'config');
|
||||
this.corePluginsDir = resolve(this.homeDir, 'core_plugins');
|
||||
this.binDir = resolve(this.homeDir, 'bin');
|
||||
this.logDir = resolve(this.homeDir, 'log');
|
||||
this.staticFilesDir = resolve(this.homeDir, 'ui');
|
||||
}
|
||||
|
||||
public getConfigFile() {
|
||||
const defaultConfigFile = this.getDefaultConfigFile();
|
||||
return this.options.config === undefined ? defaultConfigFile : this.options.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public getLegacyKbnServer(): LegacyKbnServer | undefined {
|
||||
return this.options.kbnServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets information about Kibana package (version, build number etc.).
|
||||
*/
|
||||
public getPackageInfo() {
|
||||
return this.options.packageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets mode Kibana currently run in (development or production).
|
||||
*/
|
||||
public getMode() {
|
||||
return this.options.mode;
|
||||
}
|
||||
|
||||
private getDefaultConfigFile() {
|
||||
return resolve(this.configDir, 'kibana.yml');
|
||||
}
|
||||
}
|
34
src/core/server/config/index.ts
Normal file
34
src/core/server/config/index.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This is a name of configuration node that is specifically dedicated to
|
||||
* the configuration values used by the new platform only. Eventually all
|
||||
* its nested values will be migrated to the stable config and this node
|
||||
* will be deprecated.
|
||||
*/
|
||||
export const NEW_PLATFORM_CONFIG_ROOT = '__newPlatform';
|
||||
|
||||
export { ConfigService } from './config_service';
|
||||
export { RawConfigService } from './raw_config_service';
|
||||
export { RawConfig } from './raw_config';
|
||||
/** @internal */
|
||||
export { ObjectToRawConfigAdapter } from './object_to_raw_config_adapter';
|
||||
export { Env } from './env';
|
||||
export { ConfigWithSchema } from './config_with_schema';
|
61
src/core/server/config/object_to_raw_config_adapter.ts
Normal file
61
src/core/server/config/object_to_raw_config_adapter.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { get, has, set } from 'lodash';
|
||||
|
||||
import { ConfigPath } from './config_service';
|
||||
import { RawConfig } from './raw_config';
|
||||
|
||||
/**
|
||||
* Allows plain javascript object to behave like `RawConfig` instance.
|
||||
* @internal
|
||||
*/
|
||||
export class ObjectToRawConfigAdapter implements RawConfig {
|
||||
constructor(private readonly rawValue: { [key: string]: any }) {}
|
||||
|
||||
public has(configPath: ConfigPath) {
|
||||
return has(this.rawValue, configPath);
|
||||
}
|
||||
|
||||
public get(configPath: ConfigPath) {
|
||||
return get(this.rawValue, configPath);
|
||||
}
|
||||
|
||||
public set(configPath: ConfigPath, value: any) {
|
||||
set(this.rawValue, configPath, value);
|
||||
}
|
||||
|
||||
public getFlattenedPaths() {
|
||||
return [...flattenObjectKeys(this.rawValue)];
|
||||
}
|
||||
}
|
||||
|
||||
function* flattenObjectKeys(
|
||||
obj: { [key: string]: any },
|
||||
path: string = ''
|
||||
): IterableIterator<string> {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
yield path;
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const newPath = path !== '' ? `${path}.${key}` : key;
|
||||
yield* flattenObjectKeys(value, newPath);
|
||||
}
|
||||
}
|
||||
}
|
52
src/core/server/config/raw_config.ts
Normal file
52
src/core/server/config/raw_config.ts
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { ConfigPath } from './config_service';
|
||||
|
||||
/**
|
||||
* Represents raw config store.
|
||||
*/
|
||||
export interface RawConfig {
|
||||
/**
|
||||
* Returns whether or not there is a config value located at the specified path.
|
||||
* @param configPath Path to locate value at.
|
||||
* @returns Whether or not a value exists at the path.
|
||||
*/
|
||||
has(configPath: ConfigPath): boolean;
|
||||
|
||||
/**
|
||||
* Returns config value located at the specified path.
|
||||
* @param configPath Path to locate value at.
|
||||
* @returns Config value.
|
||||
*/
|
||||
get(configPath: ConfigPath): any;
|
||||
|
||||
/**
|
||||
* Sets config value at the specified path.
|
||||
* @param configPath Path to set value for.
|
||||
* @param value Value to set for the specified path.
|
||||
*/
|
||||
set(configPath: ConfigPath, value: any): void;
|
||||
|
||||
/**
|
||||
* Returns full flattened list of the config paths that config contains.
|
||||
* @returns List of the string config paths.
|
||||
*/
|
||||
getFlattenedPaths(): string[];
|
||||
}
|
97
src/core/server/config/raw_config_service.ts
Normal file
97
src/core/server/config/raw_config_service.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { isEqual, isPlainObject } from 'lodash';
|
||||
import typeDetect from 'type-detect';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
filter,
|
||||
k$,
|
||||
map,
|
||||
Observable,
|
||||
skipRepeats,
|
||||
} from '../../lib/kbn_observable';
|
||||
|
||||
import { ObjectToRawConfigAdapter } from './object_to_raw_config_adapter';
|
||||
import { RawConfig } from './raw_config';
|
||||
import { getConfigFromFile } from './read_config';
|
||||
|
||||
// Used to indicate that no config has been received yet
|
||||
const notRead = Symbol('config not yet read');
|
||||
|
||||
export class RawConfigService {
|
||||
/**
|
||||
* The stream of configs read from the config file. Will be the symbol
|
||||
* `notRead` before the config is initially read, and after that it can
|
||||
* potentially be `null` for an empty yaml file.
|
||||
*
|
||||
* This is the _raw_ config before any overrides are applied.
|
||||
*
|
||||
* As we have a notion of a _current_ config we rely on a BehaviorSubject so
|
||||
* every new subscription will immediately receive the current config.
|
||||
*/
|
||||
private readonly rawConfigFromFile$: BehaviorSubject<any> = new BehaviorSubject(notRead);
|
||||
|
||||
private readonly config$: Observable<RawConfig>;
|
||||
|
||||
constructor(readonly configFile: string) {
|
||||
this.config$ = k$(this.rawConfigFromFile$)(
|
||||
filter(rawConfig => rawConfig !== notRead),
|
||||
map(rawConfig => {
|
||||
// If the raw config is null, e.g. if empty config file, we default to
|
||||
// an empty config
|
||||
if (rawConfig == null) {
|
||||
return new ObjectToRawConfigAdapter({});
|
||||
}
|
||||
|
||||
if (isPlainObject(rawConfig)) {
|
||||
// TODO Make config consistent, e.g. handle dots in keys
|
||||
return new ObjectToRawConfigAdapter(rawConfig);
|
||||
}
|
||||
|
||||
throw new Error(`the raw config must be an object, got [${typeDetect(rawConfig)}]`);
|
||||
}),
|
||||
// We only want to update the config if there are changes to it
|
||||
skipRepeats(isEqual)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the initial Kibana config.
|
||||
*/
|
||||
public loadConfig() {
|
||||
const config = getConfigFromFile(this.configFile);
|
||||
this.rawConfigFromFile$.next(config);
|
||||
}
|
||||
|
||||
public stop() {
|
||||
this.rawConfigFromFile$.complete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-read the Kibana config.
|
||||
*/
|
||||
public reloadConfig() {
|
||||
this.loadConfig();
|
||||
}
|
||||
|
||||
public getConfig$() {
|
||||
return this.config$;
|
||||
}
|
||||
}
|
30
src/core/server/config/read_config.ts
Normal file
30
src/core/server/config/read_config.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { safeLoad } from 'js-yaml';
|
||||
|
||||
import { ensureDeepObject } from './ensure_deep_object';
|
||||
|
||||
const readYaml = (path: string) => safeLoad(readFileSync(path, 'utf8'));
|
||||
|
||||
export const getConfigFromFile = (configFile: string) => {
|
||||
const yaml = readYaml(configFile);
|
||||
return yaml == null ? yaml : ensureDeepObject(yaml);
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`#constructor throws if number of bytes is negative 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [-1024]"`;
|
||||
|
||||
exports[`#constructor throws if number of bytes is not safe 1`] = `"Value in bytes is expected to be a safe positive integer, but provided [NaN]"`;
|
||||
|
||||
exports[`#constructor throws if number of bytes is not safe 2`] = `"Value in bytes is expected to be a safe positive integer, but provided [Infinity]"`;
|
||||
|
||||
exports[`#constructor throws if number of bytes is not safe 3`] = `"Value in bytes is expected to be a safe positive integer, but provided [9007199254740992]"`;
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { ByteSizeValue } from '../';
|
||||
|
||||
describe('parsing units', () => {
|
||||
test('bytes', () => {
|
||||
expect(ByteSizeValue.parse('123b').getValueInBytes()).toBe(123);
|
||||
});
|
||||
|
||||
test('kilobytes', () => {
|
||||
expect(ByteSizeValue.parse('1kb').getValueInBytes()).toBe(1024);
|
||||
expect(ByteSizeValue.parse('15kb').getValueInBytes()).toBe(15360);
|
||||
});
|
||||
|
||||
test('megabytes', () => {
|
||||
expect(ByteSizeValue.parse('1mb').getValueInBytes()).toBe(1048576);
|
||||
});
|
||||
|
||||
test('gigabytes', () => {
|
||||
expect(ByteSizeValue.parse('1gb').getValueInBytes()).toBe(1073741824);
|
||||
});
|
||||
|
||||
test('throws an error when no unit specified', () => {
|
||||
expect(() => ByteSizeValue.parse('123')).toThrowError('could not parse byte size value');
|
||||
});
|
||||
|
||||
test('throws an error when unsupported unit specified', () => {
|
||||
expect(() => ByteSizeValue.parse('1tb')).toThrowError('could not parse byte size value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#constructor', () => {
|
||||
test('throws if number of bytes is negative', () => {
|
||||
expect(() => new ByteSizeValue(-1024)).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('throws if number of bytes is not safe', () => {
|
||||
expect(() => new ByteSizeValue(NaN)).toThrowErrorMatchingSnapshot();
|
||||
expect(() => new ByteSizeValue(Infinity)).toThrowErrorMatchingSnapshot();
|
||||
expect(() => new ByteSizeValue(Math.pow(2, 53))).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('accepts 0', () => {
|
||||
const value = new ByteSizeValue(0);
|
||||
expect(value.getValueInBytes()).toBe(0);
|
||||
});
|
||||
|
||||
test('accepts safe positive integer', () => {
|
||||
const value = new ByteSizeValue(1024);
|
||||
expect(value.getValueInBytes()).toBe(1024);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isGreaterThan', () => {
|
||||
test('handles true', () => {
|
||||
const a = ByteSizeValue.parse('2kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(a.isGreaterThan(b)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles false', () => {
|
||||
const a = ByteSizeValue.parse('2kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(b.isGreaterThan(a)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isLessThan', () => {
|
||||
test('handles true', () => {
|
||||
const a = ByteSizeValue.parse('2kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(b.isLessThan(a)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles false', () => {
|
||||
const a = ByteSizeValue.parse('2kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(a.isLessThan(b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#isEqualTo', () => {
|
||||
test('handles true', () => {
|
||||
const a = ByteSizeValue.parse('1kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(b.isEqualTo(a)).toBe(true);
|
||||
});
|
||||
|
||||
test('handles false', () => {
|
||||
const a = ByteSizeValue.parse('2kb');
|
||||
const b = ByteSizeValue.parse('1kb');
|
||||
expect(a.isEqualTo(b)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#toString', () => {
|
||||
test('renders to nearest lower unit by default', () => {
|
||||
expect(ByteSizeValue.parse('1b').toString()).toBe('1b');
|
||||
expect(ByteSizeValue.parse('10b').toString()).toBe('10b');
|
||||
expect(ByteSizeValue.parse('1023b').toString()).toBe('1023b');
|
||||
expect(ByteSizeValue.parse('1024b').toString()).toBe('1kb');
|
||||
expect(ByteSizeValue.parse('1025b').toString()).toBe('1kb');
|
||||
expect(ByteSizeValue.parse('1024kb').toString()).toBe('1mb');
|
||||
expect(ByteSizeValue.parse('1024mb').toString()).toBe('1gb');
|
||||
expect(ByteSizeValue.parse('1024gb').toString()).toBe('1024gb');
|
||||
});
|
||||
|
||||
test('renders to specified unit', () => {
|
||||
expect(ByteSizeValue.parse('1024b').toString('b')).toBe('1024b');
|
||||
expect(ByteSizeValue.parse('1kb').toString('b')).toBe('1024b');
|
||||
expect(ByteSizeValue.parse('1mb').toString('kb')).toBe('1024kb');
|
||||
expect(ByteSizeValue.parse('1mb').toString('b')).toBe('1048576b');
|
||||
expect(ByteSizeValue.parse('512mb').toString('gb')).toBe('0.5gb');
|
||||
});
|
||||
});
|
108
src/core/server/config/schema/byte_size_value/index.ts
Normal file
108
src/core/server/config/schema/byte_size_value/index.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export type ByteSizeValueUnit = 'b' | 'kb' | 'mb' | 'gb';
|
||||
|
||||
const unitMultiplier: { [unit: string]: number } = {
|
||||
b: Math.pow(1024, 0),
|
||||
gb: Math.pow(1024, 3),
|
||||
kb: Math.pow(1024, 1),
|
||||
mb: Math.pow(1024, 2),
|
||||
};
|
||||
|
||||
function renderUnit(value: number, unit: string) {
|
||||
const prettyValue = Number(value.toFixed(2));
|
||||
return `${prettyValue}${unit}`;
|
||||
}
|
||||
|
||||
export class ByteSizeValue {
|
||||
public static parse(text: string): ByteSizeValue {
|
||||
const match = /([1-9][0-9]*)(b|kb|mb|gb)/.exec(text);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`could not parse byte size value [${text}]. value must start with a ` +
|
||||
`number and end with bytes size unit, e.g. 10kb, 23mb, 3gb, 239493b`
|
||||
);
|
||||
}
|
||||
|
||||
const value = parseInt(match[1], 0);
|
||||
const unit = match[2];
|
||||
|
||||
return new ByteSizeValue(value * unitMultiplier[unit]);
|
||||
}
|
||||
|
||||
constructor(private readonly valueInBytes: number) {
|
||||
if (!Number.isSafeInteger(valueInBytes) || valueInBytes < 0) {
|
||||
throw new Error(
|
||||
`Value in bytes is expected to be a safe positive integer, ` +
|
||||
`but provided [${valueInBytes}]`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public isGreaterThan(other: ByteSizeValue): boolean {
|
||||
return this.valueInBytes > other.valueInBytes;
|
||||
}
|
||||
|
||||
public isLessThan(other: ByteSizeValue): boolean {
|
||||
return this.valueInBytes < other.valueInBytes;
|
||||
}
|
||||
|
||||
public isEqualTo(other: ByteSizeValue): boolean {
|
||||
return this.valueInBytes === other.valueInBytes;
|
||||
}
|
||||
|
||||
public getValueInBytes(): number {
|
||||
return this.valueInBytes;
|
||||
}
|
||||
|
||||
public toString(returnUnit?: ByteSizeValueUnit) {
|
||||
let value = this.valueInBytes;
|
||||
let unit = `b`;
|
||||
|
||||
for (const nextUnit of ['kb', 'mb', 'gb']) {
|
||||
if (unit === returnUnit || (returnUnit == null && value < 1024)) {
|
||||
return renderUnit(value, unit);
|
||||
}
|
||||
|
||||
value = value / 1024;
|
||||
unit = nextUnit;
|
||||
}
|
||||
|
||||
return renderUnit(value, unit);
|
||||
}
|
||||
}
|
||||
|
||||
export const bytes = (value: number) => new ByteSizeValue(value);
|
||||
export const kb = (value: number) => bytes(value * 1024);
|
||||
export const mb = (value: number) => kb(value * 1024);
|
||||
export const gb = (value: number) => mb(value * 1024);
|
||||
export const tb = (value: number) => gb(value * 1024);
|
||||
|
||||
export function ensureByteSizeValue(value?: ByteSizeValue | string | number) {
|
||||
if (typeof value === 'string') {
|
||||
return ByteSizeValue.parse(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return new ByteSizeValue(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
61
src/core/server/config/schema/duration/index.ts
Normal file
61
src/core/server/config/schema/duration/index.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { Duration, duration as momentDuration, DurationInputArg2, isDuration } from 'moment';
|
||||
export { Duration, isDuration };
|
||||
|
||||
const timeFormatRegex = /^(0|[1-9][0-9]*)(ms|s|m|h|d|w|M|Y)$/;
|
||||
|
||||
function stringToDuration(text: string) {
|
||||
const result = timeFormatRegex.exec(text);
|
||||
if (!result) {
|
||||
throw new Error(
|
||||
`Failed to parse [${text}] as time value. ` +
|
||||
`Format must be <count>[ms|s|m|h|d|w|M|Y] (e.g. '70ms', '5s', '3d', '1Y')`
|
||||
);
|
||||
}
|
||||
|
||||
const count = parseInt(result[1], 0);
|
||||
const unit = result[2] as DurationInputArg2;
|
||||
|
||||
return momentDuration(count, unit);
|
||||
}
|
||||
|
||||
function numberToDuration(numberMs: number) {
|
||||
if (!Number.isSafeInteger(numberMs) || numberMs < 0) {
|
||||
throw new Error(
|
||||
`Failed to parse [${numberMs}] as time value. ` +
|
||||
`Value should be a safe positive integer number.`
|
||||
);
|
||||
}
|
||||
|
||||
return momentDuration(numberMs);
|
||||
}
|
||||
|
||||
export function ensureDuration(value?: Duration | string | number) {
|
||||
if (typeof value === 'string') {
|
||||
return stringToDuration(value);
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return numberToDuration(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { relative } from 'path';
|
||||
import { SchemaError } from '..';
|
||||
|
||||
/**
|
||||
* Make all paths in stacktrace relative.
|
||||
*/
|
||||
export const cleanStack = (stack: string) =>
|
||||
stack
|
||||
.split('\n')
|
||||
.filter(line => !line.includes('node_modules/') && !line.includes('internal/'))
|
||||
.map(line => {
|
||||
const parts = /.*\((.*)\).?/.exec(line) || [];
|
||||
|
||||
if (parts.length === 0) {
|
||||
return line;
|
||||
}
|
||||
|
||||
const path = parts[1];
|
||||
return line.replace(path, relative(process.cwd(), path));
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
// TODO This is skipped because it fails depending on Node version. That might
|
||||
// not be a problem, but I think we should wait with including this test until
|
||||
// we've made a proper decision around error handling in the new platform, see
|
||||
// https://github.com/elastic/kibana/issues/12947
|
||||
test.skip('includes stack', () => {
|
||||
try {
|
||||
throw new SchemaError('test');
|
||||
} catch (e) {
|
||||
expect(cleanStack(e.stack)).toMatchSnapshot();
|
||||
}
|
||||
});
|
23
src/core/server/config/schema/errors/index.ts
Normal file
23
src/core/server/config/schema/errors/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export { SchemaError } from './schema_error';
|
||||
export { SchemaTypeError } from './schema_type_error';
|
||||
export { SchemaTypesError } from './schema_types_error';
|
||||
export { ValidationError } from './validation_error';
|
31
src/core/server/config/schema/errors/schema_error.ts
Normal file
31
src/core/server/config/schema/errors/schema_error.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
export class SchemaError extends Error {
|
||||
public cause?: Error;
|
||||
|
||||
constructor(message: string, cause?: Error) {
|
||||
super(message);
|
||||
this.cause = cause;
|
||||
|
||||
// Set the prototype explicitly, see:
|
||||
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
Object.setPrototypeOf(this, SchemaError.prototype);
|
||||
}
|
||||
}
|
30
src/core/server/config/schema/errors/schema_type_error.ts
Normal file
30
src/core/server/config/schema/errors/schema_type_error.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you 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.
|
||||
*/
|
||||
|
||||
import { SchemaError } from '.';
|
||||
|
||||
export class SchemaTypeError extends SchemaError {
|
||||
constructor(error: Error | string, public readonly path: string[]) {
|
||||
super(typeof error === 'string' ? error : error.message);
|
||||
|
||||
// Set the prototype explicitly, see:
|
||||
// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
Object.setPrototypeOf(this, SchemaTypeError.prototype);
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue