cache magic - use cache and network except when a result of popstate
This commit is contained in:
parent
2250ad5571
commit
91a2061342
|
@ -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'
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
display: flex;
|
||||
color: grey;
|
||||
font-size: 90%;
|
||||
min-width: 18px;
|
||||
}
|
||||
|
||||
.skeleton .other {
|
||||
|
|
|
@ -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} />
|
||||
}
|
||||
|
||||
|
|
|
@ -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 />
|
||||
}
|
||||
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue