cache magic - use cache and network except when a result of popstate

This commit is contained in:
keyan 2021-09-06 17:36:08 -05:00
parent 2250ad5571
commit 91a2061342
15 changed files with 153 additions and 111 deletions

View File

@ -1,7 +1,7 @@
import { UserInputError, AuthenticationError } from 'apollo-server-micro'
import { ensureProtocol } from '../../lib/url'
import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from './cursor'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino'

View File

@ -1,5 +1,5 @@
import { AuthenticationError } from 'apollo-server-micro'
import { decodeCursor, LIMIT, nextCursorEncoded } from './cursor'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
export default {
Query: {
@ -83,7 +83,9 @@ export default {
})
const { checkedNotesAt } = await models.user.findUnique({ where: { id: me.id } })
await models.user.update({ where: { id: me.id }, data: { checkedNotesAt: new Date() } })
if (decodedCursor.offset === 0) {
await models.user.update({ where: { id: me.id }, data: { checkedNotesAt: new Date() } })
}
return {
lastChecked: checkedNotesAt,

View File

@ -8,11 +8,12 @@ import { useRouter } from 'next/router'
export default function CommentsFlat ({ variables, ...props }) {
const router = useRouter()
const { loading, error, data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, {
variables
const { error, data, fetchMore } = useQuery(MORE_FLAT_COMMENTS, {
variables,
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) return <div>Failed to load!</div>
if (loading) {
if (!data) {
return <CommentsFlatSkeleton />
}
const { moreFlatComments: { comments, cursor } } = data

View File

@ -1,4 +1,5 @@
import { useQuery } from '@apollo/client'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import Comment, { CommentSkeleton } from './comment'
@ -21,10 +22,13 @@ export function CommentsSkeleton () {
}
export function CommentsQuery ({ query, ...props }) {
const { loading, error, data } = useQuery(query)
const router = useRouter()
const { error, data } = useQuery(query, {
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) return <div>Failed to load!</div>
if (loading) {
if (!data) {
return <CommentsSkeleton />
}

View File

@ -6,7 +6,6 @@ import { useRouter } from 'next/router'
import { Button, Container, NavDropdown } from 'react-bootstrap'
import Price from './price'
import { useMe } from './me'
import { useApolloClient } from '@apollo/client'
import Head from 'next/head'
import { signOut, signIn, useSession } from 'next-auth/client'
import { useLightning } from './lightning'
@ -17,12 +16,21 @@ function WalletSummary ({ me }) {
return `${me.sats} \\ ${me.stacked}`
}
function RefreshableLink ({ href, children, ...props }) {
const router = useRouter()
const same = router.asPath === href
return (
<Link href={same ? `${href}?key=${Math.random()}` : href} as={href} {...props}>
{children}
</Link>
)
}
export default function Header () {
const [session, loading] = useSession()
const router = useRouter()
const path = router.asPath.split('?')[0]
const me = useMe()
const client = useApolloClient()
const Corner = () => {
if (loading) {
@ -40,27 +48,23 @@ export default function Header () {
<Link href={'/' + session.user.name} passHref>
<NavDropdown.Item>profile</NavDropdown.Item>
</Link>
<Link href='/notifications' passHref>
<NavDropdown.Item onClick={() => {
// when it's a fresh click evict old notification cache
client.cache.evict({ id: 'ROOT_QUERY', fieldName: 'notifications' })
}}
>
<RefreshableLink href='/notifications' passHref>
<NavDropdown.Item>
notifications
{me && me.hasNewNotes &&
<div className='p-1 d-inline-block bg-danger ml-1'>
<span className='invisible'>{' '}</span>
</div>}
</NavDropdown.Item>
</Link>
</RefreshableLink>
<Link href='/wallet' passHref>
<NavDropdown.Item>wallet</NavDropdown.Item>
</Link>
<div>
<NavDropdown.Divider />
<Link href='/recent' passHref>
<RefreshableLink href='/recent' passHref>
<NavDropdown.Item>recent</NavDropdown.Item>
</Link>
</RefreshableLink>
{session
? (
<Link href='/post' passHref>
@ -100,16 +104,16 @@ export default function Header () {
<Container className='px-sm-0'>
<Navbar className={styles.navbar}>
<Nav className='w-100 justify-content-between flex-wrap align-items-center' activeKey={path}>
<Link href='/' passHref>
<RefreshableLink href='/' passHref>
<Navbar.Brand className={`${styles.brand} d-none d-sm-block`}>STACKER NEWS</Navbar.Brand>
</Link>
<Link href='/' passHref>
</RefreshableLink>
<RefreshableLink href='/' passHref>
<Navbar.Brand className={`${styles.brand} d-block d-sm-none`}>SN</Navbar.Brand>
</Link>
</RefreshableLink>
<Nav.Item className='d-md-flex d-none'>
<Link href='/recent' passHref>
<RefreshableLink href='/recent' passHref>
<Nav.Link className={styles.navLink}>recent</Nav.Link>
</Link>
</RefreshableLink>
</Nav.Item>
<Nav.Item className='d-md-flex d-none'>
{session

View File

@ -48,6 +48,7 @@
display: flex;
color: grey;
font-size: 90%;
min-width: 18px;
}
.skeleton .other {

View File

@ -4,13 +4,16 @@ import Item, { ItemSkeleton } from './item'
import styles from './items.module.css'
import { MORE_ITEMS } from '../fragments/items'
import { useState } from 'react'
import { useRouter } from 'next/router'
export default function Items ({ variables, rank }) {
const { loading, error, data, fetchMore } = useQuery(MORE_ITEMS, {
variables
const router = useRouter()
const { error, data, fetchMore } = useQuery(MORE_ITEMS, {
variables,
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) return <div>Failed to load!</div>
if (loading) {
if (!data) {
return <ItemsSkeleton rank={rank} />
}

View File

@ -1,4 +1,4 @@
import { useApolloClient, useQuery } from '@apollo/client'
import { useQuery } from '@apollo/client'
import Button from 'react-bootstrap/Button'
import { useState } from 'react'
import Comment, { CommentSkeleton } from './comment'
@ -7,25 +7,22 @@ import { NOTIFICATIONS } from '../fragments/notifications'
import styles from './notifications.module.css'
import { useRouter } from 'next/router'
function Notification ({ key, n }) {
function Notification ({ n }) {
const router = useRouter()
const client = useApolloClient()
return (
<div
key={key}
className={styles.clickToContext}
onClick={() => {
if (n.__typename === 'Reply' || !n.item.title) {
// evict item from cache so that it has current state
// e.g. if they previously visited before a recent comment
client.cache.evict({ id: `Item:${n.item.parentId}` })
router.push({
pathname: '/items/[id]',
query: { id: n.item.parentId, commentId: n.item.id }
}, `/items/${n.item.parentId}`)
} else {
client.cache.evict({ id: `Item:${n.item.id}` })
router.push(`items/${n.item.id}`)
router.push({
pathname: '/items/[id]',
query: { id: n.item.id }
}, `/items/${n.item.id}`)
}
}}
>
@ -48,11 +45,13 @@ function Notification ({ key, n }) {
}
export default function Notifications ({ variables }) {
const { loading, error, data, fetchMore } = useQuery(NOTIFICATIONS, {
variables
const router = useRouter()
const { error, data, fetchMore } = useQuery(NOTIFICATIONS, {
variables,
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) return <div>Failed to load!</div>
if (loading) {
if (!data) {
return <CommentsFlatSkeleton />
}

61
lib/apollo.js Normal file
View File

@ -0,0 +1,61 @@
import { ApolloClient, InMemoryCache } from '@apollo/client'
import { isFirstPage } from './cursor'
export default new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
moreItems: {
keyArgs: ['sort', 'userId'],
merge (existing, incoming) {
if (incoming.cursor && isFirstPage(incoming.cursor)) {
return incoming
}
return {
cursor: incoming.cursor,
items: [...(existing?.items || []), ...incoming.items]
}
}
},
moreFlatComments: {
keyArgs: ['userId'],
merge (existing, incoming) {
if (incoming.cursor && isFirstPage(incoming.cursor)) {
return incoming
}
return {
cursor: incoming.cursor,
comments: [...(existing?.comments || []), ...incoming.comments]
}
}
},
notifications: {
keyArgs: false,
merge (existing, incoming) {
if (incoming.cursor && isFirstPage(incoming.cursor)) {
return incoming
}
return {
cursor: incoming.cursor,
notifications: [...(existing?.notifications || []), ...incoming.notifications],
lastChecked: incoming.lastChecked
}
}
}
}
}
}
}),
defaultOptions: {
// cache-and-network allows us to refresh pages on navigation
watchQuery: {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first'
}
}
})

View File

@ -14,3 +14,8 @@ export function nextCursorEncoded (cursor) {
cursor.offset += LIMIT
return Buffer.from(JSON.stringify(cursor)).toString('base64')
}
export function isFirstPage (cursor) {
const decursor = decodeCursor(cursor)
return decursor.offset === LIMIT
}

View File

@ -1,80 +1,31 @@
import '../styles/globals.scss'
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'
import { ApolloProvider } from '@apollo/client'
import { Provider } from 'next-auth/client'
import { FundErrorModal, FundErrorProvider } from '../components/fund-error'
import { MeProvider } from '../components/me'
import PlausibleProvider from 'next-plausible'
import { LightningProvider } from '../components/lightning'
const client = new ApolloClient({
uri: '/api/graphql',
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
moreItems: {
keyArgs: ['sort', 'userId'],
merge (existing, incoming, { readField }) {
const items = existing ? existing.items : []
return {
cursor: incoming.cursor,
items: [...items, ...incoming.items]
}
},
read (existing) {
if (existing) {
return {
cursor: existing.cursor,
items: existing.items
}
}
}
},
moreFlatComments: {
keyArgs: ['userId'],
merge (existing, incoming, { readField }) {
const comments = existing ? existing.comments : []
return {
cursor: incoming.cursor,
comments: [...comments, ...incoming.comments]
}
},
read (existing) {
if (existing) {
return {
cursor: existing.cursor,
comments: existing.comments
}
}
}
},
notifications: {
merge (existing, incoming, { readField }) {
const notifications = existing ? existing.notifications : []
return {
cursor: incoming.cursor,
notifications: [...notifications, ...incoming.notifications],
lastChecked: incoming.lastChecked
}
},
read (existing) {
return existing
}
}
}
}
}
})
})
import apolloClient from '../lib/apollo'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
function MyApp ({ Component, pageProps }) {
const router = useRouter()
useEffect(() => {
router.beforePopState(({ url, as, options }) => {
// we need to tell the next page to use a cache-first fetch policy ...
// so that scroll position can be maintained
const fullurl = new URL(url, 'https://stacker.news')
fullurl.searchParams.set('cache', true)
router.push(`${fullurl.pathname}${fullurl.search}`, as, options)
return false
})
}, [])
return (
<PlausibleProvider domain='stacker.news' trackOutboundLinks>
<Provider session={pageProps.session}>
<ApolloProvider client={client}>
<ApolloProvider client={apolloClient}>
<MeProvider>
<LightningProvider>
<FundErrorProvider>

View File

@ -1,10 +1,13 @@
import Layout from '../components/layout'
import Items from '../components/items'
import { useRouter } from 'next/router'
export default function Index () {
const router = useRouter()
console.log(router)
return (
<Layout>
<Items variables={{ sort: 'hot' }} rank />
<Items variables={{ sort: 'hot' }} rank key={router.query.key} />
</Layout>
)
}

View File

@ -11,6 +11,7 @@ import styles from '../../styles/item.module.css'
import Seo from '../../components/seo'
import ApolloClient from '../../api/client'
import { NOFOLLOW_LIMIT } from '../../lib/constants'
import { useRouter } from 'next/router'
// ssr the item without comments so that we can populate metatags
export async function getServerSideProps ({ req, params: { id } }) {
@ -68,10 +69,13 @@ export default function FullItem ({ item }) {
}
function LoadItem ({ query }) {
const { loading, error, data } = useQuery(query)
const router = useRouter()
const { error, data } = useQuery(query, {
fetchPolicy: router.query.cache ? 'cache-first' : undefined
})
if (error) return <div>Failed to load!</div>
if (loading) {
if (!data) {
return (
<div>
<ItemSkeleton>

View File

@ -1,10 +1,12 @@
import { useRouter } from 'next/router'
import Layout from '../components/layout'
import Notifications from '../components/notifications'
export default function NotificationPage () {
const router = useRouter()
return (
<Layout>
<Notifications />
<Notifications key={router.query.key} />
</Layout>
)
}

View File

@ -1,10 +1,12 @@
import Layout from '../components/layout'
import Items from '../components/items'
import { useRouter } from 'next/router'
export default function Index () {
const router = useRouter()
return (
<Layout>
<Items variables={{ sort: 'recent' }} rank />
<Items variables={{ sort: 'recent' }} rank key={router.query.key} />
</Layout>
)
}