Forking google-maps-services-js

Author: Ho Yin Cheng

/

Created: February 13, 2018

I’ve forked and made minor pull requests in the past, but this past week (a full 3 days of analyzing/coding & 1 day of documentation), I made my first real fork of an existing project. It was quite the task as I had to learn the ins and outs of the existing codebase in order to make the large changes I wanted. Today, I’ll be talking about my entire thought process as I forked google-maps-services-js into react-native-google-maps-services.

Motivations

I wanted to use the Google Places API, specifically Nearby Search, in the React Native app that I’ve been building and mentioning in my devlogs. The problem was that I was unable to find a satisfactory client library to use (something I’ll talk about later). So, I was left with the choice of either writing one from scratch or forking an existing repository and changing it to suit my needs. In the spirit of open source, I decided that forking was the way to go (it’s also a lot less work ;).

Background: The API

When people talk about Google Places API, there are actually several different parts to the API serving different functionality. This can get confusing when you search for information on how to use the API as you can get conflicting information as each has somewhat different functionality. It also gets very annoying when looking for client libraries as all of them use a generic name of “Google Places” but are actually built for only some of the APIs.

On top of that, the Places API is a subset of the APIs groups under the Google Maps API name. This can cause a lot of confusion when looking for information on how to use the different libraries.

Then on top of that, there are slightly different APIs exposed by the Android and iOS libraries. Both of which can be accessed by React Native if you choose to go the native module route.

Lastly, on top of that(!), there are different APIs exposed by the Places Library in Maps JavaScript API and others. There’s a lot to dig through and it’s made all the more confusing by the reuse of names. For my purposes, I’ll only dive into the web services APIs and a bit into the mobile APIs.

Google Maps API Hierarchy for Web Services

Important Notes

Looking for a Client Library

This was a multi-day effort of reading docs, diving into source code for poorly documented projects, and testing integration with React Native. This is what I discovered:

Conclusion

Nothing works.

Exploring All Options

Before going down the route of writing or forking a client library, it’s prudent to examine your original project needs to see if this is the best use of your time. And so, that’s what I did.

Alternatives to Google

Some suggestions and comparisons to read:

These are the best of the best. Foursquare is the most attractive offering. Mapbox is great too as it would just be a part of their total offering (you can maybe even remove Google Maps completely). Agolia is a big name, but I’d rather go with Foursquare if I weren’t to try and use a completely Mapbox-only solution. Lastly, OSM is included because it’s the one self-hostable solution.

  1. Foursquare
    • They seem to be the largest consensus provider that is used by notable companies like Instagram.
    • Like Google, they named it Places API. It has very comparable functionality.
    • Their docs are also quite clean.
    • They have no official libraries, but you can build one and submit it to them to be listed on their website.
  2. Agolia Places
    • It too uses the same naming and provides a lot of the same functionality as Google.
  3. Mapbox
  4. Nominatim
    • This is the OpenStreetMaps API.
    • You can actually download all the OSM data and setup a server yourself that allows for geocoding and reverse geocoding. It’s quite a heavy process though so rarely worth it.

Conclusion

While these certainly do provide APIs that have the functionality I desire, none have an official React Native client library. Foursquare comes the closest but the libraries do not look very well maintained. As such, I felt that my time was better spent going with Google for now as it has the most robust looking code base for me to work off of.

If in the future I decide that I want to remove all Google services, I can probably reuse what I’ve learned from their codebase to write a more robust client library for Foursquare/Mapbox/etc.

Forking

With that out of the way, I made my decision to fork google-maps-services-js. I had to fork because:

Given that rationale, it was an easy decision to make. Furthermore, Github makes forking so easy that it seemed like a no brainer over trying to write from the ground up. Do keep in mind that while you work on your fork, the upstream repository may get some commits pulled in that you need to merge into your fork. I found these guides to be the most useful in understanding how to do this and how to write my commit messages:

Source Code Deep Dive

These are the parts of the google-maps-services-js source that use core Node.js modules:

These will all have to be swapped out and replaced with a library like axios or superagent. A lot of the interface code, validation code, and even parts of the client side rate limiting should be reusable and remain untouched if done right.

Worklog: Rewriting with axios

Using my deep dive notes, I went through each task systematically to make sure that I didn’t miss anything that needed editing. Checklists for the win! You can follow along with my notes below as I explain the rationale behind all of my code edits.

End Result

Showing 15 changed files with 92 additions and 160 deletions.

Explicit Dependencies Added:

Running Tests

React Native

Writing up some test code and putting the API through its paces in React Native showed that everything worked! Except geolocatewhy?

It turns out that geolocate is no longer functional due to these outstanding issues/PR in axios: #723 #1342 #1121. Tracing the code all the way back, it looks like this bug was introduced in a combination of this commit and this commit that eventually changed the merge order of precedence like so:

  1. get default < config
  2. this.defaultConfig < get default < config
  3. defaults < this.defaults < get default < config

So it was a consequence of not thinking through the history of changes and how axios recommends editing configs. The PR should fix this by changing the order to this:

For now, all I can do is wait to bump the axios version one the fix is merged and released.

Spec Tests

Running the tests showed that some specs are failing. Meanwhile, all of Google’s spec tests pass.

My Spec Test Results

  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99100101102103104105106107108109110111112113114115116117118119
npm test

> @google/maps@0.4.5 test /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js
> jasmine && jasmine spec/e2e/*.js

Started
......F.............FFFF....F................................FFF.................

Failures:
1) attempt (when the second attempt succeeds) calls the callback with the successful result
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

2) index.js: using a client ID and secret generates a signature param
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

3) index.js: using a client ID and secret includes the channel if specified
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

4) index.js: using a language param can set the language per client
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

5) index.js: using a language param can override the language per method
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

6) index.js: throttling spaces out requests made too close
  Message:
    Expected [ 0, 0, 60000 ] to equal [ 0, 0, 0, 30 ].
  Stack:
    Error: Expected [ 0, 0, 60000 ] to equal [ 0, 0, 0, 30 ].
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/spec/unit/index-spec.js:210:30
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/make-api-call.js:150:29
        at Immediate.<anonymous> (/Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/task.js:247:27)
        at runCallback (timers.js:781:20)

7) throttle doesn't wait when calls are made far apart
  Message:
    Expected 1002000 to be 2000.
  Stack:
    Error: Expected 1002000 to be 2000.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/spec/unit/throttled-queue-spec.js:152:33
        at Immediate.<anonymous> (/Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/task.js:238:27)
        at runCallback (timers.js:781:20)
        at tryOnImmediate (timers.js:743:5)

8) throttle does not wait for calls that are cancelled
  Message:
    Expected 1000000 to be 0.
  Stack:
    Error: Expected 1000000 to be 0.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/spec/unit/throttled-queue-spec.js:165:31
        at Immediate.<anonymous> (/Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/task.js:238:27)
        at runCallback (timers.js:781:20)
        at tryOnImmediate (timers.js:743:5)
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

9) throttle when limit is 3 waits before making the 4th call made together
  Message:
    Expected 1000000 to be 0.
  Stack:
    Error: Expected 1000000 to be 0.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/spec/unit/throttled-queue-spec.js:201:33
        at Immediate.<anonymous> (/Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/task.js:238:27)
        at runCallback (timers.js:781:20)
        at tryOnImmediate (timers.js:743:5)
  Message:
    Expected 1000000 to be 0.
  Stack:
    Error: Expected 1000000 to be 0.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/spec/unit/throttled-queue-spec.js:206:33
        at Immediate.<anonymous> (/Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js/lib/internal/task.js:238:27)
        at runCallback (timers.js:781:20)
        at tryOnImmediate (timers.js:743:5)
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

81 specs, 9 failures
Finished in 35.193 seconds
npm ERR! Test failed.  See above for more details.

So why is this the case? My initial guess was that this had to do with using setImmediate as the tests had to do with timing and task.js. A quick edit of the original source to setImmediate and a run later showed this to be partially the case:

 1 2 3 4 5 6 7 8 9101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
npm test

> @google/maps@0.4.5 test /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original
> jasmine && jasmine spec/e2e/*.js

Started
............................F.......................F..........F.................

Failures:
1) index.js: throttling spaces out requests made too close
  Message:
    Expected [ 0, 0, 0 ] to equal [ 0, 0, 0, 30 ].
  Stack:
    Error: Expected [ 0, 0, 0 ] to equal [ 0, 0, 0, 30 ].
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/spec/unit/index-spec.js:210:30
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/lib/internal/make-api-call.js:146:29
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/lib/internal/task.js:247:27
        at _combinedTickCallback (internal/process/next_tick.js:131:7)

2) Task.race cancels the second task if the first task finishes
  Message:
    Expected spy cancelled2 to have been called.
  Stack:
    Error: Expected spy cancelled2 to have been called.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/spec/unit/task-spec.js:206:26
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/lib/internal/task.js:238:27
        at _combinedTickCallback (internal/process/next_tick.js:131:7)
        at process._tickCallback (internal/process/next_tick.js:180:9)

3) throttle when limit is 3 waits before making the 4th call made together
  Message:
    Expected 1000000 to be 0.
  Stack:
    Error: Expected 1000000 to be 0.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/spec/unit/throttled-queue-spec.js:201:33
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/lib/internal/task.js:238:27
        at _combinedTickCallback (internal/process/next_tick.js:131:7)
        at process._tickCallback (internal/process/next_tick.js:180:9)
  Message:
    Expected 1000000 to be 0.
  Stack:
    Error: Expected 1000000 to be 0.
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/spec/unit/throttled-queue-spec.js:206:33
        at /Users/mike/WorkspaceOSS/ReactNative/google-maps-services-js-original/lib/internal/task.js:238:27
        at _combinedTickCallback (internal/process/next_tick.js:131:7)
        at process._tickCallback (internal/process/next_tick.js:180:9)
  Message:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
  Stack:
    Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
        at ontimeout (timers.js:469:11)
        at tryOnTimeout (timers.js:304:5)
        at Timer.listOnTimeout (timers.js:264:5)

81 specs, 3 failures
Finished in 5.147 seconds
npm ERR! Test failed.  See above for more details.

So for comparison, that’s this difference which doesn’t align perfectly:

12345
# My Fork
......F.............FFFF....F................................FFF.................

# Google
............................F.......................F..........F.................

This is an oddity that is going to require a lot more investigation.

End-to-End Tests

For the e2e tests, you’ll find that Google has not updated this library in a long time as the majority of their tests fail. When compared to my fork, you’ll actually get the same errors (+1 for removing the deprecated method):

 1 2 3 4 5 6 7 8 9101112
....FFFFFFFFFF.FFFFFFF(node:44556) DeprecationWarning: placesRadar is deprecated, see http://goo.gl/BGiumE
.F...FFFFFFF

# Cutting out the deprecated test results in:
....FFFFFFFFFF.FFFFFFF.F...FFFFFFF
34 specs, 25 failures
Finished in 85.355 seconds

# Which you can compare to the rewrite that results in:
....FFFFFFFFFF.FFFFFFFFF...FFFFFFF
34 specs, 26 failures
Finished in 83.239 seconds

I’m not sure how I’ll be tackling these errors as I have no passing baseline to compare against.

Result: Usable But Needs Work

Overall, I would say that while work still needs to tbe done, the code is at least usable for my current purposes. If you would like to use this, be aware of the work I’ll be tackling in Future Tasks.

Now to make this fork usable by others without clashing with the original, I had to rename it on Github:

Then publish it on npm with the new name:

And voila! You can now install and use this package with:

1
yarn add react-native-google-maps-services

But a disclaimer: There are lots of outstanding issues that I need to fix before tagging this with a production ready release. So use this at your own risk!!

Forking an publishing projects under a new name isn’t all that simple though. There’s a lot of legal stuff and etiquette that I had to read up on first before I was sure that I should/can do this. IANAL but I’m pretty sure that the Apache 2.0 license that Google uses is ok with this given a bunch of caveats that I have to follow: [1] [2]

Also, remember that you’re building on the shoulders of giants when you fork and use open source work. Do your best to give back, but also be free to build on top of prior with with proper accreditation of course! [1] [2]

Future Tasks

You can check the README for a list of immediate tasks that I’ll be tackling. However, in addition to those, I am also considering the following:

There’s more that can also be done like using it with React, Angular, etc. I’ve also run into some interesting articles on what you can do when implementing the Google Maps APIs. They’re good source for looking at features can be added after exposing the APIs themselves:

However, all of these extras are outside of my skillset and the current scope of the project. They’re all fun things to think about and hopefully I can tackle them in the future. On that note, I’ll end this post by saying that my first foray into open source has been super fun!


Copyright © 2011-2020 Ho Yin Cheng