* Poll failed invoices with visibility timeout * Don't return intermediate failed invoices * Don't retry too old invoices * Retry invoices on client * Only attempt payment 3 times * Fix fallbacks during last retry * Rename retry column to paymentAttempt * Fix no index used * Resolve TODOs * Use expiring locks * Better comments for constants * Acquire lock during retry * Use expiring lock in retry mutation * Use now() instead of CURRENT_TIMESTAMP * Cosmetic changes * Immediately show failed post payments in notifications * Update hasNewNotes * Never retry on user cancel For a consistent UX and less mental overhead, I decided to remove the exception for ITEM_CREATE where it would still retry in the background even though we want to show the payment failure immediately in notifications. * Fix notifications without pending retries missing if no send wallets If a stacker has no send wallets, they would miss notifications about failed payments because they would never get retried. This commit fixes this by making the notifications query aware if the stacker has send wallets. This way, it can tell if a notification will be retried or not. * Stop hiding userCancel in notifications As mentioned in a previous commit, I want to show anything that will not be attempted anymore in notifications. Before, I wanted to hide manually cancelled invoices but to not change experience unnecessarily and to decrease mental overhead, I changed my mind. * Also consider invoice.cancelledAt in notifications * Always retry failed payments, even without send wallets * Fix notification indicator on retry timeout * Set invoice.updated_at to date slightly in the future * Use default job priority * Stop retrying after one hour * Remove special case for ITEM_CREATE * Replace retryTimeout job with notification indicator query * Fix sortTime --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
352 lines
11 KiB
JavaScript
352 lines
11 KiB
JavaScript
import { ApolloClient, InMemoryCache, HttpLink, makeVar, split, from } from '@apollo/client'
|
|
import { BatchHttpLink } from '@apollo/client/link/batch-http'
|
|
import { decodeCursor, LIMIT } from './cursor'
|
|
import { COMMENTS_LIMIT, SSR } from './constants'
|
|
import { RetryLink } from '@apollo/client/link/retry'
|
|
function isFirstPage (cursor, existingThings, limit = LIMIT) {
|
|
if (cursor) {
|
|
const decursor = decodeCursor(cursor)
|
|
return decursor.offset === limit
|
|
} else {
|
|
// we don't have anything cached, or our existing items are less than
|
|
// or equal to a full page
|
|
return existingThings?.length < limit
|
|
}
|
|
}
|
|
|
|
const defaultFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
|
const defaultNextFetchPolicy = SSR ? 'cache-only' : 'cache-first'
|
|
|
|
export default function getApolloClient () {
|
|
if (SSR) {
|
|
return getClient(`${process.env.SELF_URL}/api/graphql`)
|
|
} else {
|
|
window.apolloClient ||= getClient('/api/graphql')
|
|
return window.apolloClient
|
|
}
|
|
}
|
|
|
|
export const meAnonSats = {}
|
|
|
|
const retryLink = new RetryLink({
|
|
delay: {
|
|
initial: 300,
|
|
max: 30000,
|
|
jitter: true
|
|
},
|
|
attempts: {
|
|
max: Infinity,
|
|
retryIf: (error, _operation) => {
|
|
return !!error
|
|
}
|
|
}
|
|
})
|
|
|
|
function getClient (uri) {
|
|
const link = from([
|
|
retryLink,
|
|
split(
|
|
// batch zaps if wallet is enabled so they can be executed serially in a single request
|
|
operation => operation.operationName === 'act' && operation.variables.act === 'TIP' && operation.getContext().batch,
|
|
new BatchHttpLink({ uri, batchInterval: 1000, batchDebounce: true, batchMax: 0, batchKey: op => op.variables.id }),
|
|
new HttpLink({ uri })
|
|
)
|
|
])
|
|
|
|
return new ApolloClient({
|
|
link,
|
|
ssrMode: SSR,
|
|
connectToDevTools: process.env.NODE_ENV !== 'production',
|
|
cache: new InMemoryCache({
|
|
freezeResults: true,
|
|
// https://github.com/apollographql/apollo-client/issues/7648
|
|
possibleTypes: {
|
|
PaidAction: [
|
|
'ItemPaidAction',
|
|
'ItemActPaidAction',
|
|
'PollVotePaidAction',
|
|
'SubPaidAction',
|
|
'DonatePaidAction',
|
|
'ReceivePaidAction'
|
|
],
|
|
Notification: [
|
|
'Reply',
|
|
'Votification',
|
|
'Mention',
|
|
'Invitification',
|
|
'Earn',
|
|
'JobChange',
|
|
'InvoicePaid',
|
|
'WithdrawlPaid',
|
|
'Referral',
|
|
'Streak',
|
|
'FollowActivity',
|
|
'ForwardedVotification',
|
|
'Revenue',
|
|
'SubStatus',
|
|
'TerritoryPost',
|
|
'TerritoryTransfer',
|
|
'Reminder',
|
|
'ItemMention',
|
|
'Invoicification'
|
|
]
|
|
},
|
|
typePolicies: {
|
|
Sub: {
|
|
keyFields: ['name'],
|
|
fields: {
|
|
optional: {
|
|
merge: true
|
|
}
|
|
}
|
|
},
|
|
User: {
|
|
// https://www.apollographql.com/docs/react/caching/cache-field-behavior/#merging-non-normalized-objects
|
|
fields: {
|
|
privates: {
|
|
merge: true
|
|
},
|
|
optional: {
|
|
merge: true
|
|
},
|
|
bio: {
|
|
merge: true
|
|
}
|
|
}
|
|
},
|
|
Fact: {
|
|
keyFields: ['id', 'type']
|
|
},
|
|
Wallet: {
|
|
fields: {
|
|
vaultEntries: {
|
|
replace: true
|
|
}
|
|
}
|
|
},
|
|
Query: {
|
|
fields: {
|
|
sub: {
|
|
keyArgs: ['name'],
|
|
merge (existing, incoming) {
|
|
return incoming
|
|
}
|
|
},
|
|
topSubs: {
|
|
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
|
|
merge (existing, incoming, { args }) {
|
|
if (isFirstPage(incoming.cursor, existing?.subs, args.limit)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
subs: [...(existing?.subs || []), ...incoming.subs]
|
|
}
|
|
}
|
|
},
|
|
topUsers: {
|
|
keyArgs: ['when', 'by', 'from', 'to', 'limit'],
|
|
merge (existing, incoming, { args }) {
|
|
if (isFirstPage(incoming.cursor, existing?.users, args.limit)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
users: [...(existing?.users || []), ...incoming.users]
|
|
}
|
|
}
|
|
},
|
|
mySubscribedUsers: {
|
|
keyArgs: false,
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.users)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
users: [...(existing?.users || []), ...incoming.users]
|
|
}
|
|
}
|
|
},
|
|
myMutedUsers: {
|
|
keyArgs: false,
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.users)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
users: [...(existing?.users || []), ...incoming.users]
|
|
}
|
|
}
|
|
},
|
|
userSuggestions: {
|
|
keyArgs: ['q', 'limit'],
|
|
merge (existing, incoming) {
|
|
return incoming
|
|
}
|
|
},
|
|
topCowboys: {
|
|
keyArgs: [],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.users)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
users: [...(existing?.users || []), ...incoming.users]
|
|
}
|
|
}
|
|
},
|
|
items: {
|
|
keyArgs: ['sub', 'sort', 'type', 'name', 'when', 'by', 'from', 'to', 'limit'],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.items)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
items: [...(existing?.items || []), ...incoming.items],
|
|
pins: [...(existing?.pins || []), ...(incoming.pins || [])],
|
|
ad: incoming?.ad || existing?.ad
|
|
}
|
|
}
|
|
},
|
|
related: {
|
|
keyArgs: ['id', 'title', 'minMatch', 'limit'],
|
|
merge (existing, incoming, { args }) {
|
|
if (isFirstPage(incoming.cursor, existing?.items, args.limit)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
items: [...(existing?.items || []), ...incoming.items]
|
|
}
|
|
}
|
|
},
|
|
search: {
|
|
keyArgs: ['q', 'sub', 'sort', 'what', 'when', 'from', 'to', 'limit'],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.items)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
items: [...(existing?.items || []), ...incoming.items]
|
|
}
|
|
}
|
|
},
|
|
searchUsers: {
|
|
keyArgs: ['q', 'limit', 'similarity'],
|
|
merge (existing, incoming) {
|
|
return [...(existing?.searchUsers || []), ...incoming]
|
|
}
|
|
},
|
|
notifications: {
|
|
keyArgs: ['inc'],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.notifications)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
earn: existing?.earn || incoming.earn,
|
|
notifications: [...(existing?.notifications || []), ...incoming.notifications],
|
|
lastChecked: existing?.lastChecked || incoming.lastChecked
|
|
}
|
|
}
|
|
},
|
|
numBolt11s: {
|
|
keyArgs: [],
|
|
merge (existing, incoming) {
|
|
return incoming
|
|
}
|
|
},
|
|
walletHistory: {
|
|
keyArgs: ['inc'],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.facts)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
facts: [...(existing?.facts || []), ...incoming.facts]
|
|
}
|
|
}
|
|
},
|
|
failedInvoices: {
|
|
keyArgs: [],
|
|
merge (existing, incoming) {
|
|
return incoming
|
|
}
|
|
}
|
|
}
|
|
},
|
|
Item: {
|
|
fields: {
|
|
comments: {
|
|
keyArgs: ['sort'],
|
|
merge (existing, incoming) {
|
|
if (isFirstPage(incoming.cursor, existing?.comments, COMMENTS_LIMIT)) {
|
|
return incoming
|
|
}
|
|
|
|
return {
|
|
cursor: incoming.cursor,
|
|
comments: [...(existing?.comments || []), ...incoming.comments]
|
|
}
|
|
}
|
|
},
|
|
meAnonSats: {
|
|
read (existingAmount, { readField }) {
|
|
if (SSR) return null
|
|
|
|
const itemId = readField('id')
|
|
|
|
// we need to use reactive variables such that updates
|
|
// to local state propagate correctly
|
|
// see https://www.apollographql.com/docs/react/local-state/reactive-variables
|
|
let reactiveVar = meAnonSats[itemId]
|
|
if (!reactiveVar) {
|
|
const storageKey = `TIP-item:${itemId}`
|
|
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
|
reactiveVar = makeVar(existingAmount || 0)
|
|
meAnonSats[itemId] = reactiveVar
|
|
}
|
|
|
|
return reactiveVar()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}),
|
|
assumeImmutableResults: true,
|
|
queryDeduplication: true,
|
|
defaultOptions: {
|
|
watchQuery: {
|
|
initialFetchPolicy: defaultFetchPolicy,
|
|
fetchPolicy: defaultFetchPolicy,
|
|
nextFetchPolicy: defaultNextFetchPolicy,
|
|
ssr: SSR
|
|
},
|
|
query: {
|
|
initialFetchPolicy: defaultFetchPolicy,
|
|
fetchPolicy: defaultFetchPolicy,
|
|
nextFetchPolicy: defaultNextFetchPolicy,
|
|
ssr: SSR
|
|
}
|
|
}
|
|
})
|
|
}
|