diff --git a/pkg/interface/src/logic/api/gcp.ts b/pkg/interface/src/logic/api/gcp.ts index 05791db22..4ed9e8f65 100644 --- a/pkg/interface/src/logic/api/gcp.ts +++ b/pkg/interface/src/logic/api/gcp.ts @@ -4,40 +4,20 @@ import {GcpToken} from '../types/gcp-state'; export default class GcpApi extends BaseApi { - #running = false; + // Return value resolves to the token's expiry time if successful. + refreshToken() { + return this.spider('noun', 'gcp-token', 'get-gcp-token', {}) + .then((token) => { + this.store.handleEvent({ + data: token + }); - startRefreshLoop() { - if (this.#running) { - console.error('GcpApi startRefreshLoop: already running'); - } else { - this.#running = true; - this.refreshLoop(); - } - } - - private refreshLoop() { - console.log("GcpApi refreshLoop"); - this.refreshToken().then(({accessKey, expiresIn}: GcpToken) => { - console.log("GcpApi new token"); - // XX maybe bad? - this.store.state.gcp.accessKey = accessKey; - const timeout = this.refreshInterval(expiresIn); - console.log("GcpApi refreshing in", timeout); - setTimeout(() => { - this.refreshLoop(); - }, timeout); - }); - } - - private refreshInterval(expiresIn: number) { - // Give ourselves 30 seconds for processing delays, but never refresh - // sooner than 10 minute from now. (The expiry window should be about an - // hour.) - return Math.max(600_000, expiresIn - 30_000); - } - - private async refreshToken() { - const token = await this.spider('noun', 'gcp-token', 'get-gcp-token', {}); - return token['gcp-token']; + if (token['gcp-token'] !== undefined && + token['gcp-token']['expiresIn'] !== undefined && + typeof(token['gcp-token']['expiresIn']) === 'number') { + return Promise.resolve(token['gcp-token']['expiresIn']); + } + return Promise.reject({reason: 'invalid token'}); + }); } }; diff --git a/pkg/interface/src/logic/lib/gcpManager.ts b/pkg/interface/src/logic/lib/gcpManager.ts new file mode 100644 index 000000000..c74c30b46 --- /dev/null +++ b/pkg/interface/src/logic/lib/gcpManager.ts @@ -0,0 +1,94 @@ +// Singleton that manages GCP token state. +// +// To use: +// +// 1. call setApi with a GlobalApi. +// 2. call start() to start the token refresh loop. +// +// +import GlobalApi from '../api/global'; + + +class GcpManager { + #api: GlobalApi | null = null; + + setApi(api: GlobalApi) { + this.#api = api; + } + + #running = false; + #timeoutId: number | null = null; + + start() { + if (this.#running) { + console.warn('GcpManager already running'); + return; + } + if (!this.#api) { + console.error('GcpManager must have api set'); + return; + } + this.#running = true; + this.refreshLoop(); + } + + stop() { + if (!this.#running) { + console.warn('GcpManager already stopped'); + console.assert(this.#timeoutId === null); + return; + } + this.#running = false; + if (this.#timeoutId !== null) { + clearTimeout(this.#timeoutId); + this.#timeoutId = null; + } + } + + restart() { + if (this.#running) + this.stop(); + this.start(); + } + + private refreshLoop() { + console.log('GcpManager refreshing token'); + this.#api.gcp.refreshToken() + .then( + (expiresIn: number) => { + const interval = this.refreshInterval(expiresIn); + this.refreshAfter(interval); + }) + .catch( + ({reason}) => { + console.error('GcpManager token refresh failed', reason); + this.refreshAfter(10_000); // XX backoff? + }); + } + + private refreshAfter(durationMs) { + if (!this.#running) + return; + if (this.#timeoutId !== null) { + console.warn('GcpManager already has a timeout set'); + return; + } + console.log('GcpManager refreshing after', durationMs, 'ms'); + this.#timeoutId = setTimeout(() => { + this.#timeoutId = null; + this.refreshLoop(); + }, durationMs); + } + + private refreshInterval(expiresIn: number) { + // Give ourselves 30 seconds for processing delays, but never refresh + // sooner than 30 minutes from now. (The expiry window should be about an + // hour.) + return Math.max(30 * 60_000, expiresIn - 30_000); + } +} + +const instance = new GcpManager(); +Object.freeze(instance); + +export default instance; diff --git a/pkg/interface/src/views/App.js b/pkg/interface/src/views/App.js index 13fd7d9ef..67147819a 100644 --- a/pkg/interface/src/views/App.js +++ b/pkg/interface/src/views/App.js @@ -27,6 +27,7 @@ import GlobalSubscription from '~/logic/subscription/global'; import GlobalApi from '~/logic/api/global'; import { uxToHex } from '~/logic/lib/util'; import { foregroundFromBackground } from '~/logic/lib/sigil'; +import gcpManager from '~/logic/lib/gcpManager'; import { withLocalState } from '~/logic/state/local'; @@ -78,6 +79,7 @@ class App extends React.Component { this.appChannel = new window.channel(); this.api = new GlobalApi(this.ship, this.appChannel, this.store); + gcpManager.setApi(this.api); this.subscription = new GlobalSubscription(this.store, this.api, this.appChannel); @@ -94,7 +96,7 @@ class App extends React.Component { // before the app has actually rendered, hence the timeout. this.updateTheme(this.themeWatcher); }, 500); - this.api.gcp.startRefreshLoop(); + gcpManager.start(); this.api.local.getBaseHash(); this.api.settings.getAll(); this.store.rehydrate();