cloud-run-alwayson-cpu-weather-advisory/nodejs/main.js (102 lines of code) (raw):

// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. const axios = require('axios'); const express = require('express'); const nunjucks = require('nunjucks'); const {Datastore} = require('@google-cloud/datastore'); const app = express(); app.use(express.urlencoded({extended: true})); nunjucks.configure('templates', {autoescape: true, express: app}); const DATASTORE = new Datastore(); const DEFAULT = 'CA'; const MINUTE = 60 * 1000; const PORT = process.env.PORT || 8080; const URL = 'https://api.weather.gov/alerts/active?area='; const STATES = [ 'AK', 'AL', 'AR', 'AS', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA', 'GU', 'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', 'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', 'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'PR', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VA', 'VI', 'VT', 'WA', 'WI', 'WV', 'WY', ]; app.listen(PORT, () => { console.log(`** Listening on port ${PORT}`); }); // get state alerts from cache async function getStateFromCache(state) { const query = DATASTORE.createQuery('State') .filter('__key__', '=', DATASTORE.key(['State', state])); const [results] = await DATASTORE.runQuery(query); return results.length ? results[0] : null; } // check if state alerts are in cache & "fresh" async function stateIsInCache(state) { const fma = new Date(new Date() - 15*MINUTE); // "15 minutes ago" const stateData = await getStateFromCache(state); const useCache = stateData ? (stateData.lastUpdate > fma) : false; console.log(useCache ? `** Cache fresh, use in-cache data (${state})` : `** Cache stale/missing, API fetch (${state})`) return useCache; } // fetch state weather alerts from API async function fetchState(state) { const api_rsp = await axios.get(URL + state); // call weather API const advisories = api_rsp.data.features; return advisories.map(advisory => { const prop = advisory.properties; return { // extract/format relevant weather alert data area: prop.areaDesc, headline: ('NWSheadline' in prop.parameters) ? prop.parameters.NWSheadline[0] : prop.headline, effective: prop.effective, expires: prop.expires, instructions: (!prop.instruction) ? '(none)' : prop.instruction.replace(/\n/g, ' '), } }); } // cache state weather alerts to Datastore async function cacheState(state, advisories) { const entity = { key: DATASTORE.key(['State', state]), data: { advisories: advisories, lastUpdate: new Date(), // last-fetched timestamp } }; await DATASTORE.save(entity); } // check if state in cache & fresh; fetch & cache if not async function processState(state) { if (!(await stateIsInCache(state))) { const advisories = await fetchState(state); await cacheState(state, advisories); } } // main application handler (GET/POST) app.all('/', async (req, rsp) => { let context = {meth: req.method, state: DEFAULT}; // GET: render empty form // POST: process user request, display results, render empty form if (req.method === 'POST') { let state; try { state = req.body.state.trim().slice(0, 2).toUpperCase() || DEFAULT; context.state = state; await processState(state); const stateData = await getStateFromCache(state); if (stateData) { context.advs = stateData.advisories; } else { context.error = `ERROR: problem with request for ${state}`; } } catch (ex) { const error = `ERROR: problem with request for ${state}: ${ex.toString()}`; console.error(error); context.error = error; } } rsp.render('index.html', context); }) // check each state and update cache as necessary async function updateCache() { for (let state of STATES) { await processState(state); } } // always-on CPU refreshes cache every 5 minutes // (max 3x per always-on CPU re 15 min shutdown) setInterval(() => { updateCache(); }, 5*MINUTE); module.exports = { app };