From e6213f4aca07e65179f74b0432bf143f6a0a9a44 Mon Sep 17 00:00:00 2001 From: asalem1 <19984220+asalem1@users.noreply.github.com> Date: Thu, 14 Jul 2022 13:12:48 -0700 Subject: [PATCH] feat: consolidate fastFromFlux parser changes to fromFlux (#773) --- giraffe/package.json | 4 +- giraffe/src/utils/assert.ts | 5 - giraffe/src/utils/fromFlux.test.ts | 103 +++++++++--- giraffe/src/utils/fromFlux.ts | 244 ++++++++++++----------------- stories/package.json | 2 +- yarn.lock | 23 +-- 6 files changed, 182 insertions(+), 199 deletions(-) delete mode 100644 giraffe/src/utils/assert.ts diff --git a/giraffe/package.json b/giraffe/package.json index d0767d6c..8daf28b3 100644 --- a/giraffe/package.json +++ b/giraffe/package.json @@ -1,6 +1,6 @@ { "name": "@influxdata/giraffe", - "version": "2.30.0", + "version": "2.30.1", "main": "dist/index.js", "module": "dist/index.js", "license": "MIT", @@ -46,7 +46,6 @@ "@testing-library/react": "^11.2.2", "@types/d3-array": "^2.0.0", "@types/d3-color": "^1.2.2", - "@types/d3-dsv": "^1.0.36", "@types/d3-format": "^1.3.1", "@types/d3-interpolate": "^1.3.1", "@types/d3-scale": "^2.1.1", @@ -65,7 +64,6 @@ "css-modules-typescript-loader": "^4.0.0", "d3-array": "^2.0.3", "d3-color": "^1.2.3", - "d3-dsv": "^1.1.1", "d3-format": "^1.3.2", "d3-interpolate": "^1.3.2", "d3-scale": "^3.0.0", diff --git a/giraffe/src/utils/assert.ts b/giraffe/src/utils/assert.ts deleted file mode 100644 index 16b9ba41..00000000 --- a/giraffe/src/utils/assert.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const assert = (condition: boolean, errorMessage: string) => { - if (!condition) { - throw new Error(errorMessage) - } -} diff --git a/giraffe/src/utils/fromFlux.test.ts b/giraffe/src/utils/fromFlux.test.ts index d201a0dd..855973f9 100644 --- a/giraffe/src/utils/fromFlux.test.ts +++ b/giraffe/src/utils/fromFlux.test.ts @@ -333,6 +333,7 @@ describe('fromFlux', () => { ,result,table,_start,_stop,_time,_value,_field,_measurement,code,id,magType,net,title\r ,,5,2022-06-28T13:22:34.9161857Z,2022-06-29T13:22:34.9161857Z,2022-06-28T15:09:54.052Z,3.1,cdi,earthquake,7000hkux,us7000hkux,mb,us,"M 4.6 - 107 km NNW of Te Anau, New Zealand"\r ` + /* eslint-disable */ const expected = { _field: { @@ -400,7 +401,7 @@ describe('fromFlux', () => { type: 'number', }, '_value (string)': { - data: [, , , ,'green', ,], + data: [, , , , 'green', ,], fluxDataType: 'string', name: '_value', type: 'string', @@ -485,14 +486,21 @@ describe('fromFlux', () => { type: 'number', }, title: { - data: [, , , ,'M 5.4 - 44 km W of Hengchun, Taiwan', 'M 4.6 - 107 km NNW of Te Anau, New Zealand'], + data: [ + , + , + , + , + 'M 5.4 - 44 km W of Hengchun, Taiwan', + 'M 4.6 - 107 km NNW of Te Anau, New Zealand', + ], fluxDataType: 'string', name: 'title', type: 'string', }, } - /* eslint-enable */ + /* eslint-enable */ const expectedGroupKeys = [ '_start', '_stop', @@ -517,6 +525,36 @@ describe('fromFlux', () => { expect(columns).toStrictEqual(expected) expect(fluxGroupKeyUnion).toStrictEqual(expectedGroupKeys) }) + it('parses query with newlines and hashtags', () => { + const CSV = `#group,false,false,true,false +#datatype,string,long,long,string +#default,_result,,, +,result,table,_time_reverse,_value +,,0,-1652887800000000000,"Row 1 +#newline #somehashTags https://a_link.com/giraffe" +,,0,-1652887811000000000,Row 2 +,,0,-1652888700000000000,"Row 3: 👇👇👇 // emojis +,Mew line!" +,,0,-1652889550000000000,"Row 4: +New line 1! +New line 2! +multiple new lines!` + const {table} = fromFlux(CSV) + + const expectedColumns = [ + `Row 1 +#newline #somehashTags https://a_link.com/giraffe`, + 'Row 2', + `Row 3: 👇👇👇 // emojis +,Mew line!`, + `Row 4: +New line 1! +New line 2! +multiple new lines!`, + ] + + expect(table.getColumn('_value')).toStrictEqual(expectedColumns) + }) it('can parse a Flux CSV with mismatched schemas', () => { const CSV = `#group,false,false,true,true,false,true,true,true,true,true @@ -630,7 +668,7 @@ describe('fromFlux', () => { '#group,false,false,true,true,false,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true' expect(() => { - fastFromFlux(CSV) + fromFlux(CSV) }).not.toThrow() }) @@ -860,35 +898,45 @@ there",5 expect(table.getColumn('time')).toEqual([1610972402582]) }) - - it('should parse CSV with hashtags, commas and newlines', () => { - const CSV = `#group,false,false,true,false -#datatype,string,long,long,string -#default,_result,,, -,result,table,_time_reverse,_value -,,0,-1652887800000000000,"[2022-05-18 15:30:00 UTC] @textAndCommas: Visit https://a.link/ - -,#hashtag, #another, hash tag" -,,0,-1652888700000000000,"[2022-05-18 15:45:00 UTC] @emojis: 👇👇👇 -, new line -another new line"` + it('should parse JSON data as part of the table body correctly', () => { + const CSV = `#group,false,false,true,true,false,false,true,true,true,true,true,true,true,true,true,true +#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,string,string,string,string,string,string,string,string,string,string,string +#default,_result,,,,,,,,,,,,,,, +,result,table,_start,_stop,_time,_value,_field,_measurement,env,error,errorCode,errorType,orgID,ot_trace_sampled,role,source +,,0,2022-06-15T19:05:02.361293138Z,2022-06-15T20:05:02.361293138Z,2022-06-15T19:05:05.145623698Z,"{""request"":{""organization_id"":""fc0e1bf81e62ea27"",""jwttoken"":""REDACTED"",""compiler"":{""Now"":""2022-06-15T19:05:00Z"",""query"":""import ""influxdata/influxdb/monitor""\nimport ""experimental""\nimport ""influxdata/influxdb/v1""\n\ndata = from(bucket: ""Website Monitoring Bucket"")\n |\u003e range(start: -10m)\n |\u003e filter(fn: (r) =\u003e r[""_measurement""] == ""http_response"")\n |\u003e filter(fn: (r) =\u003e r[""_field""] == ""result_code"")\n |\u003e filter(fn: (r) =\u003e r[""method""] == ""HEAD"")\n |\u003e filter(fn: (r) =\u003e r[""result""] == ""success"")\n |\u003e filter(fn: (r) =\u003e r[""server""] == ""https://influxdata.com"")\n\noption task = {name: ""Name this Check"", every: 1m, offset: 0s}\n\ncheck = {_check_id: ""0854d93f9225d000"", _check_name: ""Name this Check"", _type: ""deadman"", tags: {}}\ncrit = (r) =\u003e r[""dead""]\nmessageFn = (r) =\u003e ""Check: $\{r._check_name} is: $\{r._level}""\n\ndata |\u003e v1[""fieldsAsCols""]() |\u003e monitor[""deadman""](t: experimental[""subDuration""](from: now(), d: 90s))\n |\u003e monitor[""check""](data: check, messageFn: messageFn, crit: crit)""},""source"":""tasks"",""parameters"":null,""UseIOx"":false,""compiler_type"":""flux""},""dialect"":{},""dialect_type"":""no-content""}",request,query_log,prod01-eu-central-1,"failed to initialize execute state: could not find bucket ""Website Monitoring Bucket""",not found,user,fc0e1bf81e62ea27,false,queryd-pull-internal,tasks\ +` const {table} = fromFlux(CSV) const valueColumn = table.getColumn('_value') const expectedValueColumn = [ - `[2022-05-18 15:30:00 UTC] @textAndCommas: Visit https://a.link/ - -,#hashtag, #another, hash tag`, - `[2022-05-18 15:45:00 UTC] @emojis: 👇👇👇 -, new line -another new line`, + '{"request":{"organization_id":"fc0e1bf81e62ea27","jwttoken":"REDACTED","compiler":{"Now":"2022-06-15T19:05:00Z","query":"import "influxdata/influxdb/monitor"\n' + + 'import "experimental"\n' + + 'import "influxdata/influxdb/v1"\n' + + '\n' + + 'data = from(bucket: "Website Monitoring Bucket")\n' + + ' |> range(start: -10m)\n' + + ' |> filter(fn: (r) => r["_measurement"] == "http_response")\n' + + ' |> filter(fn: (r) => r["_field"] == "result_code")\n' + + ' |> filter(fn: (r) => r["method"] == "HEAD")\n' + + ' |> filter(fn: (r) => r["result"] == "success")\n' + + ' |> filter(fn: (r) => r["server"] == "https://influxdata.com")\n' + + '\n' + + 'option task = {name: "Name this Check", every: 1m, offset: 0s}\n' + + '\n' + + 'check = {_check_id: "0854d93f9225d000", _check_name: "Name this Check", _type: "deadman", tags: {}}\n' + + 'crit = (r) => r["dead"]\n' + + 'messageFn = (r) => "Check: ${r._check_name} is: ${r._level}"\n' + + '\n' + + 'data |> v1["fieldsAsCols"]() |> monitor["deadman"](t: experimental["subDuration"](from: now(), d: 90s))\n' + + ' |> monitor["check"](data: check, messageFn: messageFn, crit: crit)"},"source":"tasks","parameters":null,"UseIOx":false,"compiler_type":"flux"},"dialect":{},"dialect_type":"no-content"}', ] expect(valueColumn).toEqual(expectedValueColumn) }) }) + describe('fastFromFlux', () => { it('should always pass for stability checks', () => { const resp = `#group,false,false,true,true,false,false,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true,true @@ -1290,7 +1338,7 @@ describe('fastFromFlux', () => { type: 'number', }, '_value (string)': { - data: [, , , ,'green', ,], + data: [, , , , 'green', ,], fluxDataType: 'string', name: '_value', type: 'string', @@ -1375,7 +1423,14 @@ describe('fastFromFlux', () => { type: 'number', }, title: { - data: [, , , ,'M 5.4 - 44 km W of Hengchun, Taiwan', 'M 4.6 - 107 km NNW of Te Anau, New Zealand'], + data: [ + , + , + , + , + 'M 5.4 - 44 km W of Hengchun, Taiwan', + 'M 4.6 - 107 km NNW of Te Anau, New Zealand', + ], fluxDataType: 'string', name: 'title', type: 'string', diff --git a/giraffe/src/utils/fromFlux.ts b/giraffe/src/utils/fromFlux.ts index cfbac6ca..df49b2cf 100644 --- a/giraffe/src/utils/fromFlux.ts +++ b/giraffe/src/utils/fromFlux.ts @@ -1,7 +1,5 @@ -import {csvParse, csvParseRows} from 'd3-dsv' import Papa from 'papaparse' import {Table, ColumnType, FluxDataType} from '../types' -import {assert} from './assert' import {newTable} from './newTable' import {RESULT} from '../constants/columnKeys' export interface FromFluxResult { @@ -73,8 +71,6 @@ interface Columns { */ export const fromFlux = (fluxCSV: string): FromFluxResult => { const columns: Columns = {} - const fluxGroupKeyUnion = new Set() - const resultColumnNames = new Set() let tableLength = 0 try { /* @@ -110,7 +106,7 @@ export const fromFlux = (fluxCSV: string): FromFluxResult => { const prevIndex = currentIndex const nextIndex = fluxCSV .substring(currentIndex, fluxCSV.length) - .search(/\n\s*\n#/) + .search(/\n\s*\n#(?=datatype|group|default)/) if (nextIndex === -1) { chunks.push([prevIndex, fluxCSV.length - 1]) currentIndex = -1 @@ -122,15 +118,29 @@ export const fromFlux = (fluxCSV: string): FromFluxResult => { } // declaring all nested variables here to reduce memory drain - let tableText = '' - let tableData: any = [] - let annotationText = '' - let columnType: any = '' let columnKey = '' - let columnDefault: any = '' - let chunk = '' - + const fluxGroupKeyUnion = new Set() + const resultColumnNames = new Set() + let _end, _start for (const [start, end] of chunks) { + _end = end + _start = start + let annotationMode = true + + const parsed = { + group: [], + datatype: [], + default: [], + columnKey: [], + } + // we want to move the pointer to the first non-whitespace character at the end of the chunk + while (/\s/.test(fluxCSV[_end]) && _end > _start) { + _end-- + } + // we want to move the pointer to the first non-whitespace character at the start of the chunk + while (/\s/.test(fluxCSV[_start]) && _start < _end) { + _start++ + } /** * substring doesn't include the index for the end. For example: * @@ -139,111 +149,89 @@ export const fromFlux = (fluxCSV: string): FromFluxResult => { * Given the fact that we want to include the last character of the chunk * we want to add + 1 to the substring ending */ - chunk = fluxCSV.substring(start, end + 1) - const splittedChunk = chunk.split('\n') - const tableTexts = [] - const annotationTexts = [] - - splittedChunk.forEach(line => { - if (line.startsWith('#')) { - annotationTexts.push(line) - } else { - tableTexts.push(line) - } - }) - - tableText = tableTexts.join('\n').trim() - - assert( - !!tableText, - 'could not find annotation lines in Flux response; are `annotations` enabled in the Flux query `dialect` option?' - ) - - tableData = csvParse(tableText) - - annotationText = annotationTexts.join('\n').trim() - - assert( - !!annotationText, - 'could not find annotation lines in Flux response; are `annotations` enabled in the Flux query `dialect` option?' - ) - const annotationData = parseAnnotations(annotationText, tableData.columns) - - for (const columnName of tableData.columns.slice(1)) { - columnType = - TO_COLUMN_TYPE[annotationData.datatypeByColumnName[columnName]] - - assert( - !!columnType, - `encountered unknown Flux column type ${annotationData.datatypeByColumnName[columnName]}` - ) - - columnKey = `${columnName} (${columnType})` - - if (!columns[columnKey]) { - columns[columnKey] = { - name: columnName, - type: columnType, - fluxDataType: annotationData.datatypeByColumnName[columnName], - data: [], - } as Column - } - - columnDefault = annotationData.defaultByColumnName[columnName] - - for (let i = 0; i < tableData.length; i++) { - if (columnName === RESULT) { - if (columnDefault.length) { - resultColumnNames.add(columnDefault) - } else if (tableData[i][columnName].length) { - resultColumnNames.add(tableData[i][columnName]) - } - } - const value = tableData[i][columnName] || columnDefault - let result = null - - if (value === undefined) { - result = undefined - } else if (value === 'null') { - result = null - } else if (value === 'NaN') { - result = NaN - } else if (columnType === 'boolean' && value === 'true') { - result = true - } else if (columnType === 'boolean' && value === 'false') { - result = false - } else if (columnType === 'string') { - result = value - } else if (columnType === 'time') { - if (/\s/.test(value)) { - result = Date.parse(value.trim()) - } else { - result = Date.parse(value) - } - } else if (columnType === 'number') { - if (value === '') { - result = null - } else { - const parsedValue = Number(value) - result = parsedValue === parsedValue ? parsedValue : value - } + Papa.parse(fluxCSV.substring(_start, _end + 1), { + step: function(results) { + if (results.data[0] === '#group') { + parsed.group = results.data.slice(1) + } else if (results.data[0] === '#datatype') { + parsed.datatype = results.data.slice(1) + } else if (results.data[0] === '#default') { + parsed.default = results.data.slice(1) + } else if (results.data[0][0] !== '#' && annotationMode === true) { + annotationMode = false + results.data.slice(1).reduce((acc, curr, index) => { + columnKey = `${curr} (${TO_COLUMN_TYPE[parsed.datatype[index]]})` + parsed.columnKey.push(columnKey) + if (!acc[columnKey]) { + acc[columnKey] = { + name: curr, + type: TO_COLUMN_TYPE[parsed.datatype[index]], + fluxDataType: parsed.datatype[index], + data: [], + } + } + if (parsed.group[index] === 'true') { + fluxGroupKeyUnion.add(columnKey) + } + return acc + }, columns) } else { - result = null - } - - columns[columnKey].data[tableLength + i] = result - } + results.data.slice(1).forEach((data, index) => { + const value = data || parsed.default[index] + let result = null - if (annotationData.groupKey.includes(columnName)) { - fluxGroupKeyUnion.add(columnKey) - } - } + if (value === undefined) { + result = undefined + } else if (value === 'null') { + result = null + } else if (value === 'NaN') { + result = NaN + } else if ( + TO_COLUMN_TYPE[parsed.datatype[index]] === 'boolean' && + value === 'true' + ) { + result = true + } else if ( + TO_COLUMN_TYPE[parsed.datatype[index]] === 'boolean' && + value === 'false' + ) { + result = false + } else if (TO_COLUMN_TYPE[parsed.datatype[index]] === 'string') { + result = value + } else if (TO_COLUMN_TYPE[parsed.datatype[index]] === 'time') { + if (/\s/.test(value)) { + result = Date.parse(value.trim()) + } else { + result = Date.parse(value) + } + } else if (TO_COLUMN_TYPE[parsed.datatype[index]] === 'number') { + if (value === '') { + result = null + } else { + const parsedValue = Number(value) + result = parsedValue === parsedValue ? parsedValue : value + } + } else { + result = null + } - tableLength += tableData.length + if (columns[parsed.columnKey[index]] !== undefined) { + if ( + columns[parsed.columnKey[index]].name === RESULT && + result + ) { + resultColumnNames.add(result) + } + columns[parsed.columnKey[index]].data[tableLength] = result + } + }) + tableLength++ + } + }, + }) } resolveNames(columns, fluxGroupKeyUnion) - const table = Object.entries(columns).reduce( (table, [key, {name, fluxDataType, type, data}]) => { data.length = tableLength @@ -457,40 +445,6 @@ export const fastFromFlux = (fluxCSV: string): FromFluxResult => { } } -const parseAnnotations = ( - annotationData: string, - headerRow: string[] -): { - groupKey: string[] - datatypeByColumnName: {[columnName: string]: any} - defaultByColumnName: {[columnName: string]: any} -} => { - const rows = csvParseRows(annotationData) - - const groupRow = rows.find(row => row[0] === '#group') - const datatypeRow = rows.find(row => row[0] === '#datatype') - const defaultRow = rows.find(row => row[0] === '#default') - - assert(!!groupRow, 'could not find group annotation in Flux response') - assert(!!datatypeRow, 'could not find datatype annotation in Flux response') - assert(!!defaultRow, 'could not find default annotation in Flux response') - - const groupKey = groupRow.reduce( - (acc, val, i) => (val === 'true' ? [...acc, headerRow[i]] : acc), - [] - ) - - const datatypeByColumnName = datatypeRow - .slice(1) - .reduce((acc, val, i) => ({...acc, [headerRow[i + 1]]: val}), {}) - - const defaultByColumnName = defaultRow - .slice(1) - .reduce((acc, val, i) => ({...acc, [headerRow[i + 1]]: val}), {}) - - return {groupKey, datatypeByColumnName, defaultByColumnName} -} - const TO_COLUMN_TYPE: {[fluxDatatype: string]: ColumnType} = { boolean: 'boolean', unsignedLong: 'number', diff --git a/stories/package.json b/stories/package.json index 1251e07d..4a1af254 100644 --- a/stories/package.json +++ b/stories/package.json @@ -1,6 +1,6 @@ { "name": "@influxdata/giraffe-stories", - "version": "2.30.0", + "version": "2.30.1", "license": "MIT", "repository": { "type": "git", diff --git a/yarn.lock b/yarn.lock index 5b408bf1..2935ddad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2474,11 +2474,6 @@ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.4.2.tgz#944f281d04a0f06e134ea96adbb68303515b2784" integrity sha512-fYtiVLBYy7VQX+Kx7wU/uOIkGQn8aAEY8oWMoyja3N4dLd8Yf6XgSIR/4yWvMuveNOH5VShnqCgRqqh/UNanBA== -"@types/d3-dsv@^1.0.36": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.2.1.tgz#1524fee9f19d689c2f76aa0e24e230762bf96994" - integrity sha512-LLmJmjiqp/fTNEdij5bIwUJ6P6TVNk5hKM9/uk5RPO2YNgEu9XvKO0dJ7Iqd3psEdmZN1m7gB1bOsjr4HmO2BA== - "@types/d3-format@^1.3.1": version "1.4.2" resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-1.4.2.tgz#ea17bf559b71d9afd569ae9bfe4c544dab863baa" @@ -4643,7 +4638,7 @@ command-line-args@^5.1.1: lodash.camelcase "^4.3.0" typical "^4.0.0" -commander@2, commander@^2.19.0, commander@^2.20.0: +commander@^2.19.0, commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -5071,15 +5066,6 @@ d3-color@1, d3-color@^1.2.3: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-2.0.0.tgz#8d625cab42ed9b8f601a1760a389f7ea9189d62e" integrity sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ== -d3-dsv@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.2.0.tgz#9d5f75c3a5f8abd611f74d3f5847b0d4338b885c" - integrity sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g== - dependencies: - commander "2" - iconv-lite "0.4" - rw "1" - "d3-format@1 - 2": version "2.0.0" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-2.0.0.tgz#a10bcc0f986c372b729ba447382413aabf5b0767" @@ -7275,7 +7261,7 @@ humanize-ms@^1.2.1: dependencies: ms "^2.0.0" -iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -11431,11 +11417,6 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" -rw@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"