refactor(mothership): switch to new redis backed endpoints

This commit is contained in:
Alexis Tyler
2020-09-06 16:03:47 +09:30
parent 337779dc8b
commit f2ef2daf3a
9 changed files with 250 additions and 157 deletions

View File

@@ -81,6 +81,7 @@ const baseTypes = [gql`
ping: PingSubscription!
info: InfoSubscription!
pluginModule(plugin: String!, module: String!, params: JSON, result: String): PluginModuleSubscription!
online: Boolean!
}
`];
@@ -182,8 +183,9 @@ const getPluginModule = (pluginName: string, pluginModuleName: string) => {
*/
class FuncDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field: { [key: string]: any }) {
// @ts-ignore
const { args } = this;
field.resolve = async function (source, directiveArgs: { [key: string]: any }, { user }, info: { [key: string]: any }) {
field.resolve = async function (_source, directiveArgs: { [key: string]: any }, { user }, info: { [key: string]: any }) {
const {module: moduleName, result: resultType} = args;
const {plugin: pluginName, module: pluginModuleName, result: pluginType, input, ...params} = directiveArgs;
const operationType = info.operation.operation;
@@ -369,19 +371,25 @@ export const graphql = {
wsHasDisconnected(websocketId);
}
},
context: ({req, connection}) => {
if (connection) {
context: ({req, connection, ...args_}, ...args__) => {
// Normal Websocket connection
if (connection && Object.keys(connection.context).length >= 1) {
// Check connection for metadata
return {
...connection.context
};
}
const apiKey = req.headers['x-api-key'];
const user = apiKeyToUser(apiKey);
return {
user
};
// Normal HTTP connection
if (req) {
const apiKey = req.headers['x-api-key'];
const user = apiKeyToUser(apiKey);
return {
user
};
}
console.log({req, connection, args_, args__});
throw new Error('Invalid');
}
};

View File

@@ -48,6 +48,10 @@ dee.on('*', async (data: { Type: string }) => {
dee.listen();
setInterval(() => {
publish('online', 'UPDATED', true);
}, 1000);
// This needs to be fixed to run from events
setIntervalAsync(async () => {
if (!canPublishToChannel('services')) {
@@ -232,6 +236,9 @@ export const resolvers = {
hasSubscribedToChannel(context.websocketId, channel);
return pubsub.asyncIterator(channel);
}
},
online: {
...createSubscription('online')
}
},
JSON: GraphQLJSON,

View File

@@ -1,14 +1,14 @@
import fs from 'fs';
import request from 'request';
import WebSocket from 'ws';
import merge from 'deepmerge';
import { log, utils, paths, states, config } from '@unraid/core';
import { DynamixConfig } from '@unraid/core/dist/lib/types';
import { userCache, CachedServer } from './cache';
const { loadState } = utils;
const { varState } = states;
process.on('uncaughtException', console.log);
process.on('unhandledRejection', console.log);
/**
* One second in milliseconds.
*/
@@ -18,6 +18,16 @@ const ONE_SECOND = 1000;
*/
const ONE_MINUTE = 60 * ONE_SECOND;
/**
* Relay ws link.
*/
const RELAY_WS_LINK = process.env.RELAY_WS_LINK ? process.env.RELAY_WS_LINK : 'wss://relay.unraid.net';
/**
* Internal ws link.
*/
const INTERNAL_WS_LINK = process.env.INTERNAL_WS_LINK ? process.env.INTERNAL_WS_LINK : `ws+unix://${config.get('graphql-api-port')}`;
/**
* Get a number between the lowest and highest value.
* @param low Lowest value.
@@ -38,7 +48,7 @@ const backoff = (attempt: number, maxDelay: number, multiplier: number) => {
return Math.round(Math.min(delay * multiplier, maxDelay));
};
let mothership;
let relay: WebSocket;
interface WebSocketWithHeartBeat extends WebSocket {
pingTimeout?: NodeJS.Timeout
@@ -56,78 +66,20 @@ function heartbeat(this: WebSocketWithHeartBeat) {
this.pingTimeout = setTimeout(() => {
this.terminate();
}, 30000 + 1000);
}
type MessageType = 'query' | 'mutation' | 'start' | 'stop' | 'proxy-data';
interface Message {
type: MessageType;
payload: {
operationName: any;
variables: {};
query: string;
data: any;
topic?: string;
}
};
type Server = CachedServer;
type Servers = Server[];
interface ProxyMessage extends Omit<Message, 'payload'> {
type: MessageType
payload: {
topic: 'servers';
data: Servers;
}
};
const readFileIfExists = (filePath: string) => {
try {
return fs.readFileSync(filePath);
} catch {}
const isProxyMessage = (message: any): message is ProxyMessage => {
const keys = Object.keys(message.payload ?? {});
return message.payload && message.type === 'proxy-data' && keys.length === 2 && keys.includes('topic') && keys.includes('data');
};
const isServersPayload = (payload: any): payload is Servers => payload.topic === 'servers';
const forwardMessageToLocalSocket = (message: Message, apiKey: string) => {
log.debug(`Got a "${message.type}" request from mothership, forwarding to socket.`);
const port = config.get('graphql-api-port');
const localEndpoint = (!isNaN(port as number)) ? `localhost:${port}` : `unix:${port}:`;
const url = `http://${localEndpoint}/graphql`;
request.post(url, {
body: JSON.stringify({
operationName: null,
variables: {},
query: message.payload.query
}),
headers: {
Accept: '*/*',
'Content-Type': 'application/json',
'x-api-key': apiKey
}
}, (error, response) => {
if (error) {
log.error(error);
return;
}
try {
const data = JSON.parse(response.body).data;
const payload = { data };
log.debug('Replying to mothership with payload %o', payload);
mothership.send(JSON.stringify({
type: 'data',
payload
}));
} catch (error) {
log.error(error);
mothership.close();
}
});
return Buffer.from('');
};
/**
* Connect to unraid's proxy server
*/
export const connectToMothership = async (wsServer, currentRetryAttempt: number = 0) => {
export const connectToMothership = async (wsServer: WebSocket.Server, currentRetryAttempt: number = 0) => {
// Kill the last connection first
await disconnectFromMothership();
let retryAttempt = currentRetryAttempt;
@@ -137,14 +89,15 @@ export const connectToMothership = async (wsServer, currentRetryAttempt: number
}
const apiKey = loadState<DynamixConfig>(paths.get('dynamix-config')!).remote.apikey || '';
const keyFile = varState.data?.regFile ? fs.readFileSync(varState.data?.regFile).toString('base64') : '';
const keyFile = varState.data?.regFile ? readFileIfExists(varState.data?.regFile).toString('base64') : '';
const serverName = `${varState.data?.name}`;
const lanIp = states.networkState.data.find(network => network.ipaddr[0]).ipaddr[0] || '';
const machineId = `${await utils.getMachineId()}`;
let localGraphqlApi: WebSocket;
// Connect to mothership
// Connect to mothership's relay endpoint
// Keep reference outside this scope so we can disconnect later
mothership = new WebSocket('wss://proxy.unraid.net', ['graphql-ws'], {
relay = new WebSocket(RELAY_WS_LINK, ['graphql-ws'], {
headers: {
'x-api-key': apiKey,
'x-flash-guid': varState.data?.flashGuid ?? '',
@@ -155,84 +108,118 @@ export const connectToMothership = async (wsServer, currentRetryAttempt: number
}
});
mothership.on('open', function() {
log.debug('Connected to mothership.');
relay.on('open', async () => {
log.debug(`Connected to mothership's relay.`);
// Reset retry attempts
retryAttempt = 0;
// Connect mothership to the internal ws server
wsServer.emit('connection', mothership);
// Connect to the internal graphql server
localGraphqlApi = new WebSocket(INTERNAL_WS_LINK, ['graphql-ws']);
// Start ping/pong
// @ts-ignore
heartbeat.bind(this);
// Heartbeat
localGraphqlApi.on('ping', () => {
heartbeat.bind(localGraphqlApi)();
});
// Errors
localGraphqlApi.on('error', error => {
log.error('ws:local-relay', 'error', error);
});
// Connection to local graphql endpoint is "closed"
localGraphqlApi.on('close', () => {
log.debug('ws:local-relay', 'close');
});
// Connection to local graphql endpoint is "open"
localGraphqlApi.on('open', () => {
log.debug('ws:local-relay', 'open');
// Authenticate with ourselves
localGraphqlApi.send(JSON.stringify({
type: 'connection_init',
payload: {
'x-api-key': apiKey
}
}));
});
// Relay message back to mothership
localGraphqlApi.on('message', (data) => {
try {
relay.send(data);
} catch (error) {
// Relay socket is closed, close internal one
if (error.message.includes('WebSocket is not open')) {
localGraphqlApi.close();
}
}
});
});
mothership.on('close', async function (this: WebSocketWithHeartBeat) {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
// Relay is closed
relay.on('close', async function (this: WebSocketWithHeartBeat, ...args) {
try {
log.debug('Connection closed.', ...args);
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
}
// Clear all listeners before running this again
relay?.removeAllListeners();
// Close connection to local graphql endpoint
localGraphqlApi?.close();
// Reconnect
setTimeout(async () => {
await connectToMothership(wsServer, retryAttempt + 1);
}, backoff(retryAttempt, ONE_MINUTE, 5));
} catch (error) {
log.error(error);
}
// Clear all listeners before running this again
mothership?.removeAllListeners();
// Reconnect
setTimeout(async () => {
await connectToMothership(wsServer, retryAttempt + 1);
}, backoff(retryAttempt, ONE_MINUTE, 2));
});
mothership.on('error', error => {
// Mothership is down
relay.on('error', (error: NodeJS.ErrnoException) => {
// The relay is down
if (error.message.includes('502')) {
return;
}
log.error(error.message);
// Connection refused, aka couldn't connect
// This is usually because the address is wrong or offline
if (error.code === 'ECONNREFUSED') {
// @ts-expect-error
log.debug(`Couldn't connect to ${error.address}:${error.port}`);
return;
}
log.error(error);
});
mothership.on('ping', heartbeat);
relay.on('ping', heartbeat);
mothership.on('message', async (stringifiedData: string) => {
const sendMessage = (client, message, timeout = 1000) => {
try {
const message: Message = JSON.parse(stringifiedData);
// Proxy this to the http endpoint
if (message.type === 'query' || message.type === 'mutation') {
forwardMessageToLocalSocket(message, apiKey);
if (client.readyState === 0) {
setTimeout(() => {
sendMessage(client, message, timeout);
log.debug('Message sent to mothership.', message)
}, timeout);
return;
}
log.debug(`Got a "${message.type}" request from mothership, handling internally.`);
client.send(message);
} catch (error) {
log.error('Failed replying to mothership.', error);
};
};
if (isProxyMessage(message)) {
const payload = message.payload;
if (isServersPayload(payload)) {
const cachedData = userCache.get<CachedServer[]>('mine');
const newData = {
servers: payload.data
};
// If we don't have cached data just save this
if (!cachedData || cachedData.length === 0) {
userCache.set('mine', newData);
return;
}
// Loop all new servers and merge new data on top of the cached stuff
// This should mean { guid: "1", status: "offline" } should keep
// all data but update the "status" field.
const mergedData = {
servers: newData.servers.map(newServer => {
const cachedServer = cachedData?.find(cachedServer => cachedServer.guid === newServer.guid);
return cachedServer ? merge(cachedServer, newServer) : newServer;
})
};
userCache.set('mine', mergedData);
}
}
relay.on('message', async (data: string) => {
try {
sendMessage(localGraphqlApi, data);
} catch (error) {
// Something weird happened while processing the message
// This is likely a malformed message
@@ -245,11 +232,10 @@ export const connectToMothership = async (wsServer, currentRetryAttempt: number
* Disconnect from mothership.
*/
export const disconnectFromMothership = async () => {
if (mothership && mothership.readyState !== 0) {
if (relay && relay.readyState !== 0) {
log.debug('Disconnecting from the proxy server.');
try {
mothership.close();
mothership = undefined;
relay.close();
} catch {}
}
};

View File

@@ -20,11 +20,15 @@ export const publish = (channel: string, mutation: string, node?: {}) => {
};
if (!canPublishToChannel(channel)) {
// console.log(`can't post to ${channel}`);
return;
}
// Update clients
pubsub.publish(channel, data);
const fieldName = Object.keys(data)[0];
pubsub.publish(channel, {
[fieldName]: data[fieldName].node
});
};
interface RunOptions {

View File

@@ -30,7 +30,7 @@ const ONE_SECOND = 1000;
const app = express();
const port = String(config.get('graphql-api-port'));
app.use(async (req, res, next) => {
app.use(async (_req, res, next) => {
// Only get the machine ID on first request
// We do this to avoid using async in the main server function
if (!app.get('x-machine-id')) {

View File

@@ -24,11 +24,27 @@ export const getWsConectionCountInChannel = (channel: string) => {
};
export const hasSubscribedToChannel = (id: string, channel: string) => {
// Setup inital object
if (subscriptions[id] === undefined) {
subscriptions[id] = {
total: 1,
channels: [channel]
};
return;
}
subscriptions[id].total++;
subscriptions[id].channels.push(channel);
};
export const hasUnsubscribedFromChannel = (id: string, channel: string) => {
// Setup inital object
if (subscriptions[id] === undefined) {
subscriptions[id] = {
total: 0,
channels: []
};
return;
}
subscriptions[id].total--;
subscriptions[id].channels = subscriptions[id].channels.filter(existingChannel => existingChannel !== channel);
};
@@ -60,12 +76,14 @@ export const wsHasDisconnected = (id: string) => {
export const canPublishToChannel = (channel: string) => {
// No ws connections
if (getWsConectionCount() === 0) {
// log.debug('No ws connections, cannot publish');
return false;
}
// No ws connections to this channel
const channelConnectionCount = getWsConectionCountInChannel(channel);
if (channelConnectionCount === 0) {
// log.debug(`No connections to channel ${channel}`);
return false;
}

82
package-lock.json generated
View File

@@ -4,6 +4,40 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@apollo/client": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.1.3.tgz",
"integrity": "sha512-zXMiaj+dX0sgXIwEV5d/PI6B8SZT2bqlKNjZWcEXRY7NjESF5J3nd4v8KOsrhHe+A3YhNv63tIl35Sq7uf41Pg==",
"requires": {
"@types/zen-observable": "^0.8.0",
"@wry/context": "^0.5.2",
"@wry/equality": "^0.2.0",
"fast-json-stable-stringify": "^2.0.0",
"graphql-tag": "^2.11.0",
"hoist-non-react-statics": "^3.3.2",
"optimism": "^0.12.1",
"prop-types": "^15.7.2",
"symbol-observable": "^1.2.0",
"ts-invariant": "^0.4.4",
"tslib": "^1.10.0",
"zen-observable": "^0.8.14"
},
"dependencies": {
"@wry/equality": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.2.0.tgz",
"integrity": "sha512-Y4d+WH6hs+KZJUC8YKLYGarjGekBrhslDbf/R20oV+AakHPINSitHfDRQz3EGcEWc1luXYNUvMhawWtZVWNGvQ==",
"requires": {
"tslib": "^1.9.3"
}
},
"graphql-tag": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz",
"integrity": "sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA=="
}
}
},
"@apollo/protobufjs": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@apollo/protobufjs/-/protobufjs-1.0.4.tgz",
@@ -1090,6 +1124,11 @@
"integrity": "sha512-1jmXgoIyzxQSm33lYgEXvegtkhloHbed2I0QGlTN66U2F9/ExqJWSCSmaWC0IB/g1tW+IYSp+tDhcZBYB1ZGog==",
"dev": true
},
"@types/zen-observable": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
"integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg=="
},
"@typescript-eslint/eslint-plugin": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.6.0.tgz",
@@ -1264,6 +1303,14 @@
}
}
},
"@wry/context": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.5.2.tgz",
"integrity": "sha512-B/JLuRZ/vbEKHRUiGj6xiMojST1kHhu4WcreLfNN7q9DqQFrb97cWgf/kiYsPSUCAMVN0HzfFc8XjJdzgZzfjw==",
"requires": {
"tslib": "^1.9.3"
}
},
"@wry/equality": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.11.tgz",
@@ -5783,6 +5830,14 @@
"warning": "^4.0.3"
}
},
"cross-fetch": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.0.5.tgz",
"integrity": "sha512-FFLcLtraisj5eteosnX1gf01qYDCOc4fDy0+euOt8Kn9YBY2NtXL/pCoYPavw24NIQkQqm5ZOLsGD5Zzj0gyew==",
"requires": {
"node-fetch": "2.6.0"
}
},
"cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -9654,6 +9709,14 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -11057,7 +11120,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"dev": true,
"requires": {
"js-tokens": "^3.0.0 || ^4.0.0"
}
@@ -13569,6 +13631,14 @@
"integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==",
"dev": true
},
"optimism": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.12.1.tgz",
"integrity": "sha512-t8I7HM1dw0SECitBYAqFOVHoBAHEQBTeKjIL9y9ImHzAVkdyPK4ifTgM4VJRDtTUY4r/u5Eqxs4XcGPHaoPkeQ==",
"requires": {
"@wry/context": "^0.5.2"
}
},
"optional": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/optional/-/optional-0.1.4.tgz",
@@ -14290,7 +14360,6 @@
"version": "15.7.2",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
"dev": true,
"requires": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -14531,8 +14600,7 @@
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"dev": true
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"react-popper": {
"version": "1.3.7",
@@ -16198,9 +16266,9 @@
}
},
"subscriptions-transport-ws": {
"version": "0.9.16",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz",
"integrity": "sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw==",
"version": "0.9.17",
"resolved": "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz",
"integrity": "sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA==",
"requires": {
"backo2": "^1.0.2",
"eventemitter3": "^3.1.0",

View File

@@ -29,6 +29,7 @@
"index.js"
],
"dependencies": {
"@apollo/client": "^3.1.3",
"@gridplus/docker-events": "github:OmgImAlexis/docker-events",
"@unraid/core": "github:unraid/core",
"accesscontrol": "^2.2.1",
@@ -36,6 +37,7 @@
"apollo-server": "2.14.2",
"apollo-server-express": "2.14.2",
"camelcase": "6.0.0",
"cross-fetch": "^3.0.5",
"dot-prop": "^5.2.0",
"express": "^4.17.1",
"graphql": "^15.1.0",
@@ -51,7 +53,7 @@
"redact-secrets": "github:omgimalexis/redact-secrets",
"set-interval-async": "^1.0.33",
"stoppable": "^1.1.0",
"subscriptions-transport-ws": "^0.9.16"
"subscriptions-transport-ws": "^0.9.17"
},
"optionalDependencies": {},
"devDependencies": {

View File

@@ -9,10 +9,10 @@
"skipLibCheck": true,
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
"allowJs": false, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
@@ -23,7 +23,7 @@
"rootDir": "./app", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
"removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
@@ -40,10 +40,10 @@
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */