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:
Aleh Zasypkin 2018-07-11 13:12:33 +03:00 committed by GitHub
parent 5e0cddcc9e
commit f88d0b92a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
242 changed files with 18697 additions and 606 deletions

View file

@ -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

View file

@ -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",

View file

@ -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}`);
}
}

View file

@ -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();
}
}

View 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;
}

View 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);
});
});
});

View file

@ -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",

View file

@ -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
View 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
View 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';

View 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

View 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;

View 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();
});
}
}

View 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

View file

@ -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"`;

View 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);
});

View 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);
});

View 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]);
});
});

View 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']);
});
});

View 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);
}
}

View 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);
}
}

View 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';

View file

@ -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],
]
`;

View file

@ -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();
});

View 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 { $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']);
});

View file

@ -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);
});

View 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]);
});

View file

@ -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);
});

View 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]));
});
};
}

View 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;
};
});
}

View 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();
}
};
});
}

View 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);
});
}

View 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);
}

View 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();
}
});
}

View 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);
}
);
});
}

View 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';

View 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);
}

View 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';

View 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>;

View 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;
}

View file

@ -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();
});
}

View 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';

View 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;
}

View 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);
};
}

View 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';

View file

@ -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.],
]
`;

View file

@ -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.],
],
]
`;

View file

@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rejects if error received 1`] = `
Array [
Array [
[Error: fail],
],
]
`;

View file

@ -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']);
});

View file

@ -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();
});

View 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();
});

View 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]);
});

View file

@ -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']);
});

View file

@ -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']);
});

View 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']);
});

View file

@ -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]);
});

View file

@ -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']);
});

View file

@ -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();
});

View 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();
},
});
});
};
}

View 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()'));
},
});
});
};
}

View 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;
};
});
};
}

View 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';

View 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()'));
}
},
});
});
};
}

View 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();
},
});
});
};
}

View 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();
}
},
});
});
};
}

View 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);
};
}

View 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();
},
});
});
};
}

View 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();
},
});
});
};
}

View 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();
},
});
});
};
}

View 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);
};
}

View 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);
},
});
});
};
}

View 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);
}
}
}

View file

@ -0,0 +1,6 @@
Platform Server Modules
=======================
Http Server
-----------
TODO: explain

View file

@ -0,0 +1,3 @@
pid:
enabled: true
file: '/var/run/kibana.pid'

View file

@ -0,0 +1,2 @@
pid.enabled: true
pid.file: '/var/run/kibana.pid'

View 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,
},
};
}

View file

@ -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]"`;

View 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();
});

View 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) {}
}

View 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 } }]);
});

View 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);
});

View 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();
});

View 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',
},
});
});

View 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));

View 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;
}

View 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));
}

View 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');
}
}

View 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';

View 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);
}
}
}

View 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[];
}

View 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$;
}
}

View 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);
};

View file

@ -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]"`;

View 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.
*/
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');
});
});

View 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;
}

View 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;
}

View 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 { 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();
}
});

View 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';

View 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);
}
}

View 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