Compare commits
4 Commits
6e1d67b3c0
...
63e60fe2bc
Author | SHA1 | Date | |
---|---|---|---|
|
63e60fe2bc | ||
|
0aa5ba4955 | ||
|
6e8d7ef1b8 | ||
|
e57c930f0c |
@ -1,3 +1,6 @@
|
||||
PRISMA_SLOW_LOGS_MS=
|
||||
GRAPHQL_SLOW_LOGS_MS=
|
||||
|
||||
############################################################################
|
||||
# OPTIONAL SECRETS #
|
||||
# put these in .env.local, and don't commit them to git #
|
||||
|
@ -16,4 +16,6 @@ NEXT_PUBLIC_NORMAL_POLL_INTERVAL=30000
|
||||
NEXT_PUBLIC_LONG_POLL_INTERVAL=60000
|
||||
NEXT_PUBLIC_EXTRA_LONG_POLL_INTERVAL=300000
|
||||
NEXT_PUBLIC_URL=https://stacker.news
|
||||
TOR_PROXY=http://127.0.0.1:7050/
|
||||
TOR_PROXY=http://127.0.0.1:7050/
|
||||
PRISMA_SLOW_LOGS_MS=50
|
||||
GRAPHQL_SLOW_LOGS_MS=50
|
@ -6,7 +6,7 @@ const prisma = global.prisma || (() => {
|
||||
log: [{ level: 'query', emit: 'event' }, 'warn', 'error']
|
||||
})
|
||||
prisma.$on('query', (e) => {
|
||||
if (e.duration > 50) {
|
||||
if (process.env.PRISMA_SLOW_LOGS_MS && e.duration > process.env.PRISMA_SLOW_LOGS_MS) {
|
||||
console.log('Query: ' + e.query)
|
||||
console.log('Params: ' + e.params)
|
||||
console.log('Duration: ' + e.duration + 'ms')
|
||||
|
@ -130,9 +130,11 @@ async function performOptimisticAction (actionType, args, context) {
|
||||
const { models } = context
|
||||
const action = paidActions[actionType]
|
||||
|
||||
context.optimistic = true
|
||||
context.lndInvoice = await createLndInvoice(actionType, args, context)
|
||||
|
||||
return await models.$transaction(async tx => {
|
||||
context.tx = tx
|
||||
context.optimistic = true
|
||||
|
||||
const invoice = await createDbInvoice(actionType, args, context)
|
||||
|
||||
@ -160,15 +162,20 @@ async function performPessimisticAction (actionType, args, context) {
|
||||
const invoice = await verifyPayment(context)
|
||||
args.invoiceId = invoice.id
|
||||
|
||||
// make sure to perform before settling so we don't race with worker to onPaid
|
||||
const result = await action.perform(args, context)
|
||||
|
||||
// XXX this might cause the interactive tx to time out
|
||||
await settleHodlInvoice({ secret: invoice.preimage, lnd })
|
||||
|
||||
return {
|
||||
result: await action.perform(args, context),
|
||||
result,
|
||||
paymentMethod: 'PESSIMISTIC'
|
||||
}
|
||||
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
|
||||
} else {
|
||||
// just create the invoice and complete action when it's paid
|
||||
context.lndInvoice = await createLndInvoice(actionType, args, context)
|
||||
return {
|
||||
invoice: await createDbInvoice(actionType, args, context),
|
||||
paymentMethod: 'PESSIMISTIC'
|
||||
@ -201,13 +208,18 @@ export async function retryPaidAction (actionType, args, context) {
|
||||
throw new Error(`retryPaidAction - missing invoiceId ${actionType}`)
|
||||
}
|
||||
|
||||
context.optimistic = true
|
||||
context.user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
const { msatsRequested } = await models.invoice.findUnique({ where: { id: invoiceId, actionState: 'FAILED' } })
|
||||
context.cost = BigInt(msatsRequested)
|
||||
context.lndInvoice = await createLndInvoice(actionType, args, context)
|
||||
|
||||
return await models.$transaction(async tx => {
|
||||
context.tx = tx
|
||||
context.optimistic = true
|
||||
|
||||
// update the old invoice to RETRYING, so that it's not confused with FAILED
|
||||
const { msatsRequested, actionId } = await tx.invoice.update({
|
||||
const { actionId } = await tx.invoice.update({
|
||||
where: {
|
||||
id: invoiceId,
|
||||
actionState: 'FAILED'
|
||||
@ -217,7 +229,6 @@ export async function retryPaidAction (actionType, args, context) {
|
||||
}
|
||||
})
|
||||
|
||||
context.cost = BigInt(msatsRequested)
|
||||
context.actionId = actionId
|
||||
|
||||
// create a new invoice
|
||||
@ -234,13 +245,14 @@ export async function retryPaidAction (actionType, args, context) {
|
||||
const OPTIMISTIC_INVOICE_EXPIRE = { minutes: 10 }
|
||||
const PESSIMISTIC_INVOICE_EXPIRE = { minutes: 10 }
|
||||
|
||||
async function createDbInvoice (actionType, args, context) {
|
||||
const { user, models, tx, lnd, cost, optimistic, actionId } = context
|
||||
// we seperate the invoice creation into two functions because
|
||||
// because if lnd is slow, it'll timeout the interactive tx
|
||||
async function createLndInvoice (actionType, args, context) {
|
||||
const { user, lnd, cost, optimistic } = context
|
||||
const action = paidActions[actionType]
|
||||
const db = tx ?? models
|
||||
const [createLNDInvoice, expirePivot, actionState] = optimistic
|
||||
? [createInvoice, OPTIMISTIC_INVOICE_EXPIRE, 'PENDING']
|
||||
: [createHodlInvoice, PESSIMISTIC_INVOICE_EXPIRE, 'PENDING_HELD']
|
||||
const [createLNDInvoice, expirePivot] = optimistic
|
||||
? [createInvoice, OPTIMISTIC_INVOICE_EXPIRE]
|
||||
: [createHodlInvoice, PESSIMISTIC_INVOICE_EXPIRE]
|
||||
|
||||
if (cost < 1000n) {
|
||||
// sanity check
|
||||
@ -248,19 +260,33 @@ async function createDbInvoice (actionType, args, context) {
|
||||
}
|
||||
|
||||
const expiresAt = datePivot(new Date(), expirePivot)
|
||||
const lndInv = await createLNDInvoice({
|
||||
return await createLNDInvoice({
|
||||
description: user?.hideInvoiceDesc ? undefined : await action.describe(args, context),
|
||||
lnd,
|
||||
mtokens: String(cost),
|
||||
expires_at: expiresAt
|
||||
})
|
||||
}
|
||||
|
||||
async function createDbInvoice (actionType, args, context) {
|
||||
const { user, models, tx, lndInvoice, cost, optimistic, actionId } = context
|
||||
const db = tx ?? models
|
||||
const [expirePivot, actionState] = optimistic
|
||||
? [OPTIMISTIC_INVOICE_EXPIRE, 'PENDING']
|
||||
: [PESSIMISTIC_INVOICE_EXPIRE, 'PENDING_HELD']
|
||||
|
||||
if (cost < 1000n) {
|
||||
// sanity check
|
||||
throw new Error('The cost of the action must be at least 1 sat')
|
||||
}
|
||||
|
||||
const expiresAt = datePivot(new Date(), expirePivot)
|
||||
const invoice = await db.invoice.create({
|
||||
data: {
|
||||
hash: lndInv.id,
|
||||
hash: lndInvoice.id,
|
||||
msatsRequested: cost,
|
||||
preimage: optimistic ? undefined : lndInv.secret,
|
||||
bolt11: lndInv.request,
|
||||
preimage: optimistic ? undefined : lndInvoice.secret,
|
||||
bolt11: lndInvoice.request,
|
||||
userId: user?.id || USER_ID.anon,
|
||||
actionType,
|
||||
actionState,
|
||||
@ -273,7 +299,7 @@ async function createDbInvoice (actionType, args, context) {
|
||||
await db.$executeRaw`
|
||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein, priority)
|
||||
VALUES ('checkInvoice',
|
||||
jsonb_build_object('hash', ${lndInv.id}::TEXT), 21, true,
|
||||
jsonb_build_object('hash', ${lndInvoice.id}::TEXT), 21, true,
|
||||
${expiresAt}::TIMESTAMP WITH TIME ZONE,
|
||||
${expiresAt}::TIMESTAMP WITH TIME ZONE - now() + interval '10m', 100)`
|
||||
|
||||
|
@ -218,14 +218,14 @@ export async function onPaid ({ invoice, id }, context) {
|
||||
FROM ancestors, comment
|
||||
WHERE ancestors."userId" <> comment."userId"`
|
||||
|
||||
notifyItemParents({ item, me: item.userId, models }).catch(console.error)
|
||||
notifyItemParents({ item, models }).catch(console.error)
|
||||
}
|
||||
|
||||
for (const { userId } of item.mentions) {
|
||||
notifyMention({ models, item, userId }).catch(console.error)
|
||||
}
|
||||
for (const { referee } of item.itemReferrers) {
|
||||
notifyItemMention({ models, referrerItem: item, refereeItem: referee }).catch(console.error)
|
||||
for (const { refereeItem } of item.itemReferrers) {
|
||||
notifyItemMention({ models, referrerItem: item, refereeItem }).catch(console.error)
|
||||
}
|
||||
notifyUserSubscribers({ models, item }).catch(console.error)
|
||||
notifyTerritorySubscribers({ models, item }).catch(console.error)
|
||||
|
@ -133,13 +133,13 @@ export async function perform (args, context) {
|
||||
|
||||
// TODO: referals for boost
|
||||
|
||||
// notify all the mentions if the mention is new
|
||||
// compare timestamps to only notify if mention or item referral was just created to avoid duplicates on edits
|
||||
for (const { userId, createdAt } of item.mentions) {
|
||||
if (item.updatedAt.getTime() === createdAt.getTime()) continue
|
||||
if (item.updatedAt.getTime() !== createdAt.getTime()) continue
|
||||
notifyMention({ models, item, userId }).catch(console.error)
|
||||
}
|
||||
for (const { refereeItem, createdAt } of item.itemReferrers) {
|
||||
if (item.updatedAt.getTime() === createdAt.getTime()) continue
|
||||
if (item.updatedAt.getTime() !== createdAt.getTime()) continue
|
||||
notifyItemMention({ models, referrerItem: item, refereeItem }).catch(console.error)
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
|
||||
|
||||
// perform denomormalized aggregates: weighted votes, upvotes, msats, lastZapAt
|
||||
// NOTE: for the rows that might be updated by a concurrent zap, we use UPDATE for implicit locking
|
||||
await tx.$executeRaw`
|
||||
const [item] = await tx.$queryRaw`
|
||||
WITH zapper AS (
|
||||
SELECT trust FROM users WHERE id = ${itemAct.userId}::INTEGER
|
||||
), zap AS (
|
||||
@ -107,7 +107,8 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
|
||||
msats = "Item".msats + ${msats}::BIGINT,
|
||||
"lastZapAt" = now()
|
||||
FROM zap, zapper
|
||||
WHERE "Item".id = ${itemAct.itemId}::INTEGER`
|
||||
WHERE "Item".id = ${itemAct.itemId}::INTEGER
|
||||
RETURNING "Item".*`
|
||||
|
||||
// record potential bounty payment
|
||||
// NOTE: we are at least guaranteed that we see the update "ItemUserAgg" from our tx so we can trust
|
||||
@ -139,7 +140,7 @@ export async function onPaid ({ invoice, actIds }, { models, tx }) {
|
||||
WHERE "Item".path @> zapped.path AND "Item".id <> zapped.id`
|
||||
|
||||
// TODO: referrals
|
||||
notifyZapped({ models, id: itemAct.itemId }).catch(console.error)
|
||||
notifyZapped({ models, item }).catch(console.error)
|
||||
}
|
||||
|
||||
export async function onFail ({ invoice }, { tx }) {
|
||||
|
@ -88,11 +88,11 @@ export function usePaidMutation (mutation,
|
||||
hash: invoice.hash
|
||||
}
|
||||
}))
|
||||
ourOnCompleted?.(data)
|
||||
onPaid?.(client.cache, { data })
|
||||
// block until the invoice to be marked as paid
|
||||
// for pessimisitic actions, they won't show up on navigation until they are marked as paid
|
||||
await invoiceWaiter.waitUntilPaid(invoice, inv => inv?.actionState === 'PAID')
|
||||
ourOnCompleted?.(data)
|
||||
onPaid?.(client.cache, { data })
|
||||
} catch (e) {
|
||||
console.error('usePaidMutation: failed to pay invoice', e)
|
||||
onPayError?.(e, client.cache, { data })
|
||||
|
@ -1,6 +1,6 @@
|
||||
import webPush from 'web-push'
|
||||
import removeMd from 'remove-markdown'
|
||||
import { USER_ID, COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants'
|
||||
import { COMMENT_DEPTH_LIMIT, FOUND_BLURBS, LOST_BLURBS } from './constants'
|
||||
import { msatsToSats, numWithUnits } from './format'
|
||||
import models from '@/api/models'
|
||||
import { isMuted } from '@/lib/user'
|
||||
@ -178,9 +178,9 @@ export const notifyTerritorySubscribers = async ({ models, item }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyItemParents = async ({ models, item, me }) => {
|
||||
export const notifyItemParents = async ({ models, item }) => {
|
||||
try {
|
||||
const user = await models.user.findUnique({ where: { id: me?.id || USER_ID.anon } })
|
||||
const user = await models.user.findUnique({ where: { id: item.userId } })
|
||||
const parents = await models.$queryRawUnsafe(
|
||||
'SELECT DISTINCT p."userId" FROM "Item" i JOIN "Item" p ON p.path @> i.path WHERE i.id = $1 and p."userId" <> $2 ' +
|
||||
'AND NOT EXISTS (SELECT 1 FROM "Mute" m WHERE m."muterId" = p."userId" AND m."mutedId" = $2)',
|
||||
@ -198,48 +198,48 @@ export const notifyItemParents = async ({ models, item, me }) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const notifyZapped = async ({ models, id }) => {
|
||||
export const notifyZapped = async ({ models, item }) => {
|
||||
try {
|
||||
const updatedItem = await models.item.findUnique({ where: { id: Number(id) } })
|
||||
const forwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
|
||||
const forwards = await models.itemForward.findMany({ where: { itemId: item.id } })
|
||||
const userPromises = forwards.map(fwd => models.user.findUnique({ where: { id: fwd.userId } }))
|
||||
const userResults = await Promise.allSettled(userPromises)
|
||||
const mappedForwards = forwards.map((fwd, index) => ({ ...fwd, user: userResults[index].value ?? null }))
|
||||
let forwardedSats = 0
|
||||
let forwardedUsers = ''
|
||||
if (mappedForwards.length) {
|
||||
forwardedSats = Math.floor(msatsToSats(updatedItem.msats) * mappedForwards.map(fwd => fwd.pct).reduce((sum, cur) => sum + cur) / 100)
|
||||
forwardedSats = Math.floor(msatsToSats(item.msats) * mappedForwards.map(fwd => fwd.pct).reduce((sum, cur) => sum + cur) / 100)
|
||||
forwardedUsers = mappedForwards.map(fwd => `@${fwd.user.name}`).join(', ')
|
||||
}
|
||||
let notificationTitle
|
||||
if (updatedItem.title) {
|
||||
if (item.title) {
|
||||
if (forwards.length > 0) {
|
||||
notificationTitle = `your post forwarded ${numWithUnits(forwardedSats)} to ${forwardedUsers}`
|
||||
} else {
|
||||
notificationTitle = `your post stacked ${numWithUnits(msatsToSats(updatedItem.msats))}`
|
||||
notificationTitle = `your post stacked ${numWithUnits(msatsToSats(item.msats))}`
|
||||
}
|
||||
} else {
|
||||
if (forwards.length > 0) {
|
||||
// I don't think this case is possible
|
||||
notificationTitle = `your reply forwarded ${numWithUnits(forwardedSats)} to ${forwardedUsers}`
|
||||
} else {
|
||||
notificationTitle = `your reply stacked ${numWithUnits(msatsToSats(updatedItem.msats))}`
|
||||
notificationTitle = `your reply stacked ${numWithUnits(msatsToSats(item.msats))}`
|
||||
}
|
||||
}
|
||||
await sendUserNotification(updatedItem.userId, {
|
||||
|
||||
await sendUserNotification(item.userId, {
|
||||
title: notificationTitle,
|
||||
body: updatedItem.title ? updatedItem.title : updatedItem.text,
|
||||
item: updatedItem,
|
||||
tag: `TIP-${updatedItem.id}`
|
||||
body: item.title ? item.title : item.text,
|
||||
item,
|
||||
tag: `TIP-${item.id}`
|
||||
})
|
||||
|
||||
// send push notifications to forwarded recipients
|
||||
if (mappedForwards.length) {
|
||||
await Promise.allSettled(mappedForwards.map(forward => sendUserNotification(forward.user.id, {
|
||||
title: `you were forwarded ${numWithUnits(Math.round(msatsToSats(updatedItem.msats) * forward.pct / 100))}`,
|
||||
body: updatedItem.title ?? updatedItem.text,
|
||||
item: updatedItem,
|
||||
tag: `FORWARDEDTIP-${updatedItem.id}`
|
||||
title: `you were forwarded ${numWithUnits(Math.round(msatsToSats(item.msats) * forward.pct / 100))}`,
|
||||
body: item.title ?? item.text,
|
||||
item,
|
||||
tag: `FORWARDEDTIP-${item.id}`
|
||||
})))
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -26,7 +26,7 @@ const apolloServer = new ApolloServer({
|
||||
return (error, result) => {
|
||||
const end = process.hrtime.bigint()
|
||||
const ms = (end - start) / 1000000n
|
||||
if (ms > 50) {
|
||||
if (process.env.GRAPHQL_SLOW_LOGS_MS && ms > process.env.GRAPHQL_SLOW_LOGS_MS) {
|
||||
console.log(`Field ${info.parentType.name}.${info.fieldName} took ${ms}ms`)
|
||||
}
|
||||
if (error) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user