-
Notifications
You must be signed in to change notification settings - Fork 67
feat: initial draft-implementation for the 2.0 API #895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c7cdaf4
ecc641f
1569ca3
067ad78
9c59d6a
a5e4e16
1ad96c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Proposed 2.0 API | ||
|
||
The proposed API consists of just two main functions: `setOptions` to | ||
configure the options and `importLibrary` to asynchronously load a library. | ||
Both are exported as functions or with the default-export. | ||
|
||
```ts | ||
// using the default export | ||
import ApiLoader from "@googlemaps/js-api-loader"; | ||
|
||
// initial configuration | ||
ApiLoader.setOptions({ apiKey: "...", version: "weekly" }); | ||
|
||
// loading a library | ||
const { Map } = await ApiLoader.importLibrary("maps"); | ||
``` | ||
|
||
```ts | ||
// alternative, using named exports | ||
import { setOptions, importLibrary } from "@googlemaps/js-api-loader"; | ||
|
||
const { Map } = await importLibrary("maps"); | ||
``` | ||
|
||
## Internal Behavior | ||
|
||
- the ApiLoader doesn't do anything (except for storing the options) until | ||
the `importLibrary` function is called for the first time. This allows | ||
users to configure the loader in a central place of their application | ||
even if the maps API isn't used on most pages. | ||
|
||
- Once the importLibrary function is called, the options are frozen and | ||
attempts to modify them will get ignored | ||
|
||
- the first call to importLibrary initiates the bootstrapping, once the | ||
maps API is loaded, importLibrary will directly forward to the | ||
`google.maps.importLibrary` function. | ||
|
||
- if an attempt to load the API fails, the loader will resolve all pending | ||
importLibrary promises with an error and will retry loading with the next | ||
importLibrary call. This allows users to implement their own handling of | ||
unstable network conditions and the like |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { APILoadingError, type APIOptions, bootstrapLoader } from "./loader"; | ||
|
||
// fixme: remove the second importLibrary signature and ApiLibraryMap interface | ||
// once proper typings are implemented in @types/google.maps | ||
// (https://github.com/googlemaps/js-types/issues/95) | ||
|
||
interface APILibraryMap { | ||
core: google.maps.CoreLibrary; | ||
drawing: google.maps.DrawingLibrary; | ||
elevation: google.maps.ElevationLibrary; | ||
geocoding: google.maps.GeocodingLibrary; | ||
geometry: google.maps.GeometryLibrary; | ||
journeySharing: google.maps.JourneySharingLibrary; | ||
maps: google.maps.MapsLibrary; | ||
maps3d: google.maps.Maps3DLibrary; | ||
marker: google.maps.MarkerLibrary; | ||
places: google.maps.PlacesLibrary; | ||
routes: google.maps.RoutesLibrary; | ||
streetView: google.maps.StreetViewLibrary; | ||
visualization: google.maps.VisualizationLibrary; | ||
} | ||
|
||
type APILibraryName = keyof APILibraryMap; | ||
|
||
let isBootrapped_ = false; | ||
let options_: APIOptions = {}; | ||
const libraries_: Partial<APILibraryMap> = {}; | ||
|
||
/** | ||
* Sets the options for the Maps JavaScript API. | ||
* Has to be called before any library is loaded for the first time. | ||
* Will throw an error after a library has been loaded for the first time. | ||
*/ | ||
export function setOptions(options: APIOptions) { | ||
if (isBootrapped_) { | ||
return; | ||
} | ||
|
||
options_ = options; | ||
} | ||
|
||
/** | ||
* Import the specified library. | ||
*/ | ||
export async function importLibrary( | ||
...p: Parameters<typeof google.maps.importLibrary> | ||
): ReturnType<typeof google.maps.importLibrary>; | ||
|
||
export async function importLibrary< | ||
TLibraryName extends APILibraryName, | ||
TLibrary extends APILibraryMap[TLibraryName], | ||
>(libraryName: TLibraryName): Promise<TLibrary> { | ||
if (!isBootrapped_) { | ||
bootstrapLoader(options_); | ||
isBootrapped_ = true; | ||
} | ||
|
||
if (!libraries_[libraryName]) { | ||
try { | ||
const library = (await google.maps.importLibrary( | ||
libraryName | ||
)) as TLibrary; | ||
libraries_[libraryName] = library; | ||
} catch (error) { | ||
if (error instanceof APILoadingError) { | ||
isBootrapped_ = false; | ||
throw new Error("The Google Maps JavaScript API failed to load."); | ||
} | ||
|
||
throw error; | ||
} | ||
} | ||
|
||
return libraries_[libraryName] as TLibrary; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. while I like adding better typing to importLibrary, that seems like something that could be better to fix directly in the google.maps typings. also, from a usage perspective I'm wondering this should simply invoke the global There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Regarding the typings, there's already everything in place for In situations where multiple versions of this package are somehow bundled in an Application, it's going to cause a problem, since the So it would probably be a better Idea to use presence of the |
||
|
||
const api = { | ||
setOptions, | ||
importLibrary, | ||
}; | ||
|
||
export default api; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/* | ||
* NOTE: this is functionally equivalent to (and originally copied from) the | ||
* official dynamic library import script: | ||
* https://developers.google.com/maps/documentation/javascript/load-maps-js-api | ||
* | ||
* Formatting etc: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a really good job of de-minimizing! but my concern with respect to these changes is keeping them in sync over time. preferably we could set up a pipeline from the google-internal version of this to GitHub that didn't require manual intervention. we're looking into this internally There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also seeing more changes than just these (e.g. throwing instead of logging particular errors) - I think we should be careful to adhere to the same design choices though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. I don't think there will be situations where this loader and the one in the documentation have to handle things differently. I don't think there will be a lot of reasons for this to change at all (assuming the TrustedTypesPolicy could be implemented upstream). There might also be a few details that slipped through, I remember there being some things that looked strange to me in the minified code... The custom error here is just part of an attempt to handle network-errors separately from other error-conditions. |
||
* - removed IIFE parameters | ||
* - formatted code, inlined/renamed variables and functions | ||
* - fixed typescript compatibility | ||
* - slightly restructured | ||
* Functional Changes: | ||
* - added support for TrustedTypes | ||
* - add APILoadingError | ||
*/ | ||
|
||
export type APIOptions = { | ||
key?: string; | ||
v?: string; | ||
libraries?: string | string[]; | ||
language?: string; | ||
region?: string; | ||
authReferrerPolicy?: string; | ||
mapIds?: string | string[]; | ||
channel?: string; | ||
solutionChannel?: string; | ||
}; | ||
|
||
export class APILoadingError extends Error {} | ||
|
||
/** | ||
* Creates the bootstrap function for the API loader. The bootstrap function is | ||
* an initial version of `google.maps.importLibrary` that loads the actual JS | ||
* which will then replace the bootstrap-function with the actual implementation. | ||
*/ | ||
export function bootstrapLoader(options: APIOptions) { | ||
if (google.maps.importLibrary) | ||
throw new Error("bootstrapLoader can only be called once"); | ||
|
||
let apiLoadedPromise: Promise<void>; | ||
|
||
// @ts-ignore | ||
if (!window.google) window.google = {}; | ||
// @ts-ignore | ||
if (!window.google.maps) window.google.maps = {}; | ||
|
||
const libraries = new Set(); | ||
const urlParameters = new URLSearchParams(); | ||
|
||
const startLoading = () => { | ||
if (!apiLoadedPromise) { | ||
apiLoadedPromise = new Promise((resolve, reject) => { | ||
urlParameters.set("libraries", [...libraries] + ""); | ||
for (const [name, value] of Object.entries(options)) { | ||
urlParameters.set( | ||
name.replace(/[A-Z]/g, (t) => "_" + t[0].toLowerCase()), | ||
value as string | ||
); | ||
} | ||
urlParameters.set("callback", `google.maps.__ib__`); | ||
|
||
const scriptEl = document.createElement("script"); | ||
scriptEl.src = `https://maps.googleapis.com/maps/api/js?${urlParameters.toString()}`; | ||
|
||
scriptEl.onerror = () => { | ||
reject(new APILoadingError()); | ||
}; | ||
|
||
const nonceScript = | ||
document.querySelector<HTMLScriptElement>("script[nonce]"); | ||
scriptEl.nonce = nonceScript?.nonce || ""; | ||
|
||
// @ts-ignore | ||
google.maps["__ib__"] = resolve; | ||
|
||
document.head.append(scriptEl); | ||
}); | ||
} | ||
|
||
return apiLoadedPromise; | ||
}; | ||
|
||
// create the intermediate importLibrary function that loads the API and calls | ||
// the real importLibrary function. | ||
google.maps.importLibrary = async (library: string) => { | ||
libraries.add(library); | ||
|
||
await startLoading(); | ||
|
||
return google.maps.importLibrary(library); | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bootstrap is actually intended to gracefully ignore attempts to re-bootstrap (instead of throwing)
(see later note about keeping to similar design choices)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can surely do that as well.