fix: initial load (#3)
* fix: safe load from store initial * style: scaling * chore: excl. netlify * fix: proxy to bypass CORS issues * fix: refetch limiting
This commit is contained in:
parent
22ab6a3be6
commit
d4b969e0ad
9 changed files with 89 additions and 28 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -115,3 +115,6 @@ dist
|
|||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
24
netlify/functions/rss-proxy/rss-proxy.js
Executable file
24
netlify/functions/rss-proxy/rss-proxy.js
Executable file
|
@ -0,0 +1,24 @@
|
|||
const https = require('https')
|
||||
|
||||
async function httpGet(url) {
|
||||
return new Promise((resolve) => {
|
||||
https.get(url, (response) => {
|
||||
response.on('data', (d) => resolve(d))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handler = async (event) => {
|
||||
try {
|
||||
const url = event.queryStringParameters.url
|
||||
const proxiedResponse = await httpGet(url)
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: String(proxiedResponse),
|
||||
}
|
||||
} catch (error) {
|
||||
return { statusCode: 500, body: error.toString() }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { handler }
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"start": "parcel src/index.html --https --host localhost.localdomain",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:fix": "eslint src netlify --fix",
|
||||
"types": "tsc --noEmit",
|
||||
"build": "parcel build src/index.html",
|
||||
"build:bundlesize": "parcel build src/index.html --reporter @parcel/reporter-bundle-analyzer"
|
||||
|
|
|
@ -15,7 +15,7 @@ export default function App(): ReactNode {
|
|||
useEffect(() => {
|
||||
if (state.loaded) return
|
||||
|
||||
setFeedUrls(restoreSettings().feedUrls)
|
||||
setFeedUrls(restoreSettings()?.feedUrls ?? [])
|
||||
}, [state.loaded, setFeedUrls])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -8,7 +8,7 @@ import Button from '@material-ui/core/Button'
|
|||
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline'
|
||||
import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'
|
||||
|
||||
import { persistSettings } from './utils/persistence'
|
||||
import { storeSettings } from './utils/persistence'
|
||||
|
||||
interface Props {
|
||||
feedUrls: string[]
|
||||
|
@ -73,7 +73,7 @@ export default function SettingsPanel(props: Props): ReactNode {
|
|||
const validUrls = feedUrlsForm.filter(isValidUrl)
|
||||
setFeedUrls(validUrls)
|
||||
setFeedUrlsForm(validUrls)
|
||||
persistSettings({ feedUrls: validUrls })
|
||||
storeSettings({ feedUrls: validUrls })
|
||||
}}
|
||||
>
|
||||
Save
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<html>
|
||||
<head></head>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
|
||||
<style>
|
||||
body { background-color: #f5f5f5 }
|
||||
</style>
|
||||
|
|
|
@ -10,3 +10,12 @@ export interface State {
|
|||
feedUrls: string[]
|
||||
activePanel: string
|
||||
}
|
||||
|
||||
export interface RSSData {
|
||||
items: Item[]
|
||||
lastPushed: Date
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
feedUrls: string[]
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import axios from 'axios'
|
||||
import { parseFeed } from 'htmlparser2'
|
||||
import md5 from 'crypto-js/md5'
|
||||
|
||||
import type { Item } from '../types'
|
||||
|
||||
import { persistResults, restoreResults } from './persistence'
|
||||
import { restoreRssData, storeRssData } from './persistence'
|
||||
|
||||
function processFeedXML(feed) {
|
||||
return feed.items.reduce((items, feedItem) => {
|
||||
|
@ -18,6 +17,12 @@ function processFeedXML(feed) {
|
|||
}, [])
|
||||
}
|
||||
|
||||
function getRefetchThreshold() {
|
||||
const refetchThreshold = new Date()
|
||||
refetchThreshold.setMinutes(refetchThreshold.getMinutes() - 10)
|
||||
return refetchThreshold
|
||||
}
|
||||
|
||||
/*
|
||||
* Fetches RSS feeds from the given url list. If feed data exists in
|
||||
* localStorage, it is used as a basis for the final list and any new
|
||||
|
@ -32,17 +37,16 @@ export default async function fetchFeeds(
|
|||
): Item[] {
|
||||
const feed = await Promise.all(
|
||||
feedUrls.map(async (url: string) => {
|
||||
const urlHash = md5(url)
|
||||
const storedFeedData = restoreResults(urlHash)
|
||||
const storedFeedData = restoreRssData(url)
|
||||
|
||||
const { items, lastPush } = storedFeedData || { items: [] }
|
||||
const items = storedFeedData?.items || []
|
||||
const lastPush = storedFeedData?.lastPush
|
||||
|
||||
// TODO: Constantize
|
||||
if (!forceRefetch && lastPush > Date.now() - 10 * 60 * 1000)
|
||||
return items
|
||||
|
||||
const response = await axios.get(url)
|
||||
if (!forceRefetch && lastPush > getRefetchThreshold()) return items
|
||||
|
||||
const response = await axios.get('/.netlify/functions/rss-proxy', {
|
||||
params: { url },
|
||||
})
|
||||
const availableFeedItems = [...items]
|
||||
|
||||
try {
|
||||
|
@ -58,9 +62,9 @@ export default async function fetchFeeds(
|
|||
})
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e)
|
||||
console.error(e.response)
|
||||
}
|
||||
persistResults(urlHash, availableFeedItems)
|
||||
storeRssData(url, availableFeedItems)
|
||||
|
||||
return availableFeedItems
|
||||
}),
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
export function persistSettings(settings) {
|
||||
window.localStorage.setItem('settings', JSON.stringify(settings))
|
||||
import md5 from 'crypto-js/md5'
|
||||
|
||||
import type { RSSData, Settings } from '../types'
|
||||
|
||||
const SETTINGS_KEY = 'settings'
|
||||
const RSS_DATA_KEY_PREFIX = 'savedItems_'
|
||||
|
||||
function storeToLocal(key: string, data): void {
|
||||
window.localStorage.setItem(key, JSON.stringify(data))
|
||||
}
|
||||
|
||||
export function restoreSettings() {
|
||||
return JSON.parse(window.localStorage.getItem('settings') ?? {})
|
||||
function restoreFromLocal(key: string) {
|
||||
const restored = window.localStorage.getItem(key)
|
||||
try {
|
||||
return JSON.parse(restored)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
export function storeSettings(settings: Settings): void {
|
||||
return storeToLocal(SETTINGS_KEY, settings)
|
||||
}
|
||||
|
||||
export function persistResults(h: string, items: Item[]) {
|
||||
window.localStorage.setItem(
|
||||
`savedItems_${h}`,
|
||||
JSON.stringify({ items, lastPush: Date.now() }),
|
||||
)
|
||||
export function restoreSettings(): Settings {
|
||||
return restoreFromLocal(SETTINGS_KEY)
|
||||
}
|
||||
|
||||
export function restoreResults(key): Item[] {
|
||||
return JSON.parse(window.localStorage.getItem(`savedItems_${key}`))
|
||||
export function storeRssData(url: string, items: Item[]): void {
|
||||
const key = RSS_DATA_KEY_PREFIX + md5(url)
|
||||
storeToLocal(key, { items, lastPush: Date.now() })
|
||||
}
|
||||
|
||||
export function restoreRssData(url: string): RSSData {
|
||||
const key = RSS_DATA_KEY_PREFIX + md5(url)
|
||||
return restoreFromLocal(key)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue