Subscription Examples
このコンテンツはまだ日本語訳がありません。
Real-time data updates using GraphQL subscriptions over WebSockets.
Table of Contents
Section titled “Table of Contents”Apollo Client with Subscriptions
Section titled “Apollo Client with Subscriptions”import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'import { GraphQLWsLink } from '@apollo/client/link/subscriptions'import { getMainDefinition } from '@apollo/client/utilities'import { createClient } from 'graphql-ws'
const httpLink = new HttpLink({ uri: 'https://sai-keeper.testnet-2.nibiru.fi/graphql'})
const wsLink = new GraphQLWsLink( createClient({ url: 'wss://sai-keeper.testnet-2.nibiru.fi/graphql', connectionParams: { // Add auth headers if needed }, retryAttempts: 5, shouldRetry: () => true }))
// Split link based on operation typeconst splitLink = split( ({ query }) => { const definition = getMainDefinition(query) return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ) }, wsLink, httpLink)
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache()})urql with Subscriptions
Section titled “urql with Subscriptions”import { createClient, subscriptionExchange, fetchExchange } from 'urql'import { createClient as createWSClient } from 'graphql-ws'
const wsClient = createWSClient({ url: 'wss://sai-keeper.testnet-2.nibiru.fi/graphql'})
const client = createClient({ url: 'https://sai-keeper.testnet-2.nibiru.fi/graphql', exchanges: [ fetchExchange, subscriptionExchange({ forwardSubscription: (operation) => ({ subscribe: (sink) => ({ unsubscribe: wsClient.subscribe(operation, sink) }) }) }) ]})Perp Subscriptions
Section titled “Perp Subscriptions”Example 1: Watch User Trades
Section titled “Example 1: Watch User Trades”Subscribe to all trade updates for a specific user.
import { gql } from '@apollo/client'
const WATCH_TRADES = gql` subscription WatchTrades($trader: String!) { perpTrades(where: { trader: $trader }) { id isOpen isLong leverage collateralAmount openPrice closePrice sl tp perpBorrowing { baseToken { symbol logoUrl } marketId } state { pnlCollateral pnlPct liquidationPrice positionValue borrowingFeeCollateral } openBlock { block_ts } } }`
// Apollo Client usageconst subscription = client.subscribe({ query: WATCH_TRADES, variables: { trader: 'nibi1abc...' }}).subscribe({ next: ({ data }) => { console.log('Trades updated:', data.perpTrades)
data.perpTrades.forEach(trade => { const pnlPct = (trade.state.pnlPct * 100).toFixed(2) const status = trade.isOpen ? 'OPEN' : 'CLOSED'
console.log(`[${status}] ${trade.perpBorrowing.baseToken.symbol} ${trade.isLong ? 'LONG' : 'SHORT'} ${trade.leverage}x`) console.log(` PnL: ${pnlPct}%`) console.log(` Liquidation: $${trade.state.liquidationPrice.toFixed(2)}`) }) }, error: (error) => { console.error('Subscription error:', error) }})
// Cleanup// subscription.unsubscribe()Example 2: Watch Trade Events
Section titled “Example 2: Watch Trade Events”Subscribe to trade history events (opens, closes, liquidations, etc).
const WATCH_TRADE_EVENTS = gql` subscription WatchTradeEvents($trader: String!) { perpTradeHistory(where: { trader: $trader }) { id tradeChangeType realizedPnlCollateral realizedPnlPct block { block block_ts } trade { id perpBorrowing { baseToken { symbol } collateralToken { symbol decimals } } isLong leverage openPrice closePrice } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_TRADE_EVENTS, variables: { trader: 'nibi1abc...' }}).subscribe({ next: ({ data }) => { const events = data.perpTradeHistory
events.forEach(event => { const timestamp = new Date(event.block.block_ts).toLocaleTimeString() const token = event.trade?.perpBorrowing.baseToken.symbol
console.log(`[${timestamp}] ${event.tradeChangeType}`)
if (event.realizedPnlPct !== null) { const pnlPct = (event.realizedPnlPct * 100).toFixed(2) const decimals = event.trade.perpBorrowing.collateralToken.decimals const pnl = event.realizedPnlCollateral / (10 ** decimals)
console.log(` ${token} - Realized PnL: ${pnl.toFixed(2)} (${pnlPct}%)`) } })
// Show notification for important events events.forEach(event => { if (event.tradeChangeType === 'position_liquidated') { showNotification('⚠️ Position Liquidated!', { body: `Your ${event.trade?.perpBorrowing.baseToken.symbol} position was liquidated` }) }
if (event.tradeChangeType === 'position_closed_tp') { showNotification('✅ Take Profit Hit!', { body: `TP triggered on ${event.trade?.perpBorrowing.baseToken.symbol}` }) } }) }})Example 3: Watch Market Borrowing Rates
Section titled “Example 3: Watch Market Borrowing Rates”Subscribe to real-time borrowing rate updates for a specific market.
const WATCH_MARKET = gql` subscription WatchMarket($collateralId: Int!, $marketId: Int!) { perpBorrowing(collateralId: $collateralId, marketId: $marketId) { marketId baseToken { symbol } price oiLong oiShort oiMax feesPerHourLong feesPerHourShort openFeePct closeFeePct } }`
// Usageconst subscription = client.subscribe({ query: WATCH_MARKET, variables: { collateralId: 1, marketId: 1 }}).subscribe({ next: ({ data }) => { const market = data.perpBorrowing
// Calculate funding rate APR const fundingAprLong = market.feesPerHourLong * 24 * 365 * 100 const fundingAprShort = market.feesPerHourShort * 24 * 365 * 100
// Calculate OI utilization const oiTotal = market.oiLong + market.oiShort const utilization = (oiTotal / market.oiMax) * 100
console.log(`${market.baseToken.symbol} Market Update`) console.log(`Price: $${market.price.toFixed(2)}`) console.log(`OI: ${market.oiLong} L / ${market.oiShort} S`) console.log(`Utilization: ${utilization.toFixed(1)}%`) console.log(`Funding APR - Long: ${fundingAprLong.toFixed(2)}% | Short: ${fundingAprShort.toFixed(2)}%`)
// Alert on high utilization if (utilization > 90) { console.warn('⚠️ High OI utilization!') } }})Example 4: Watch All Markets
Section titled “Example 4: Watch All Markets”Subscribe to updates for all available markets.
const WATCH_ALL_MARKETS = gql` subscription WatchAllMarkets { perpBorrowings { marketId baseToken { symbol name } collateralToken { symbol } visible } }`
// Usageconst subscription = client.subscribe({ query: WATCH_ALL_MARKETS}).subscribe({ next: ({ data }) => { const markets = data.perpBorrowings.filter(m => m.visible) console.log(`Active markets: ${markets.length}`)
// Update market selector UI updateMarketList(markets) }})LP Subscriptions
Section titled “LP Subscriptions”Example 5: Watch Vault Metrics
Section titled “Example 5: Watch Vault Metrics”Subscribe to real-time vault updates (TVL, APY, share price).
const WATCH_VAULTS = gql` subscription WatchVaults { lpVaults { address collateralToken { symbol name decimals } tvl sharePrice apy availableAssets currentEpoch revenueInfo { NetProfit TraderLosses Liabilities } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_VAULTS}).subscribe({ next: ({ data }) => { data.lpVaults.forEach(vault => { const decimals = vault.collateralToken.decimals const tvl = vault.tvl / (10 ** decimals) const available = vault.availableAssets / (10 ** decimals) const utilization = ((tvl - available) / tvl) * 100
console.log(`${vault.collateralToken.symbol} Vault`) console.log(` TVL: ${tvl.toLocaleString()}`) console.log(` APY: ${vault.apy?.toFixed(2) || 'N/A'}%`) console.log(` Share Price: ${vault.sharePrice.toFixed(6)}`) console.log(` Utilization: ${utilization.toFixed(1)}%`) console.log(` Net Profit: ${vault.revenueInfo.NetProfit}`) })
// Update dashboard updateVaultDashboard(data.lpVaults) }})Example 6: Watch User LP Positions
Section titled “Example 6: Watch User LP Positions”Subscribe to user’s LP deposit updates.
const WATCH_USER_LP = gql` subscription WatchUserLP($user: String!) { lpDeposits(where: { depositor: $user }) { depositor shares vault { address collateralToken { symbol decimals } sharePrice apy } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_USER_LP, variables: { user: 'nibi1abc...' }}).subscribe({ next: ({ data }) => { let totalValue = 0
data.lpDeposits.forEach(deposit => { const decimals = deposit.vault.collateralToken.decimals const value = (deposit.shares * deposit.vault.sharePrice) / (10 ** decimals) totalValue += value
console.log(`${deposit.vault.collateralToken.symbol}: ${value.toFixed(2)}`) })
console.log(`Total LP Value: ${totalValue.toFixed(2)}`)
// Update portfolio UI updatePortfolioValue(totalValue) }})Example 7: Watch Deposit Events
Section titled “Example 7: Watch Deposit Events”Subscribe to deposit/withdrawal events.
const WATCH_DEPOSIT_EVENTS = gql` subscription WatchDepositEvents($user: String!, $vault: String!) { lpDepositHistory(where: { depositor: $user, vault: $vault }) { id amount shares isWithdraw block { block_ts } vault { collateralToken { symbol decimals } } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_DEPOSIT_EVENTS, variables: { user: 'nibi1abc...', vault: 'nibi1vault...' }}).subscribe({ next: ({ data }) => { data.lpDepositHistory.forEach(event => { const decimals = event.vault.collateralToken.decimals const amount = event.amount / (10 ** decimals) const action = event.isWithdraw ? 'Withdrew' : 'Deposited' const timestamp = new Date(event.block.block_ts).toLocaleString()
console.log(`[${timestamp}] ${action} ${amount.toFixed(2)} ${event.vault.collateralToken.symbol}`)
// Show notification showNotification(`LP ${action}`, { body: `${amount.toFixed(2)} ${event.vault.collateralToken.symbol}`, timestamp: event.block.block_ts }) }) }})Example 8: Watch Withdrawal Requests
Section titled “Example 8: Watch Withdrawal Requests”Subscribe to withdrawal request updates.
const WATCH_WITHDRAWALS = gql` subscription WatchWithdrawals($user: String!, $vault: String!) { lpWithdrawRequests(where: { depositor: $user, vault: $vault }) { shares status unlockEpoch autoRedeem vault { currentEpoch sharePrice collateralToken { symbol decimals } } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_WITHDRAWALS, variables: { user: 'nibi1abc...', vault: 'nibi1vault...' }}).subscribe({ next: ({ data }) => { data.lpWithdrawRequests.forEach(request => { const isReady = request.vault.currentEpoch >= request.unlockEpoch const epochsRemaining = Math.max(0, request.unlockEpoch - request.vault.currentEpoch)
const decimals = request.vault.collateralToken.decimals const estimatedValue = (request.shares * request.vault.sharePrice) / (10 ** decimals)
console.log(`Withdrawal Request`) console.log(` Status: ${isReady ? 'Ready ✅' : `${epochsRemaining} epochs remaining`}`) console.log(` Estimated Value: ${estimatedValue.toFixed(2)} ${request.vault.collateralToken.symbol}`) console.log(` Auto-redeem: ${request.autoRedeem}`)
// Notify when ready if (isReady && !notifiedAlready) { showNotification('✅ Withdrawal Ready!', { body: `Your withdrawal of ${estimatedValue.toFixed(2)} ${request.vault.collateralToken.symbol} is ready` }) notifiedAlready = true } }) }})Oracle Subscriptions
Section titled “Oracle Subscriptions”Example 9: Watch Token Prices
Section titled “Example 9: Watch Token Prices”Subscribe to real-time price updates.
const WATCH_PRICES = gql` subscription WatchPrices { tokenPricesUsd { token { id symbol name } priceUsd lastUpdatedBlock { block block_ts } } }`
// Usageconst priceCache = new Map()
const subscription = client.subscribe({ query: WATCH_PRICES}).subscribe({ next: ({ data }) => { data.tokenPricesUsd.forEach(item => { const oldPrice = priceCache.get(item.token.symbol) const newPrice = item.priceUsd
// Calculate price change if (oldPrice) { const change = ((newPrice - oldPrice) / oldPrice) * 100 const arrow = change > 0 ? '↑' : '↓' console.log(`${item.token.symbol}: $${newPrice.toFixed(2)} ${arrow} ${Math.abs(change).toFixed(2)}%`) } else { console.log(`${item.token.symbol}: $${newPrice.toFixed(2)}`) }
priceCache.set(item.token.symbol, newPrice) })
// Update price ticker UI updatePriceTicker(data.tokenPricesUsd) }})Example 10: Watch Specific Token Price
Section titled “Example 10: Watch Specific Token Price”const WATCH_TOKEN_PRICE = gql` subscription WatchTokenPrice($tokenId: Int!) { tokenPricesUsd(where: { tokenId: $tokenId }) { token { symbol } priceUsd lastUpdatedBlock { block_ts } } }`
// Usage with price alertsconst subscription = client.subscribe({ query: WATCH_TOKEN_PRICE, variables: { tokenId: 1 } // BTC}).subscribe({ next: ({ data }) => { const priceData = data.tokenPricesUsd[0] const price = priceData.priceUsd
console.log(`BTC: $${price.toFixed(2)}`)
// Price alerts if (price > 50000) { showNotification('🚀 BTC Above $50k!', { body: `Current price: $${price.toFixed(2)}` }) }
if (price < 45000) { showNotification('📉 BTC Below $45k', { body: `Current price: $${price.toFixed(2)}` }) } }})Example 11: Watch User Balances
Section titled “Example 11: Watch User Balances”const WATCH_BALANCES = gql` subscription WatchBalances($user: String!) { userBalances(where: { user: $user }) { amount token_info { symbol name decimals type logo } } }`
// Usageconst subscription = client.subscribe({ query: WATCH_BALANCES, variables: { user: 'nibi1abc...' }}).subscribe({ next: ({ data }) => { console.log('Balance Update:')
data.userBalances.forEach(balance => { const amount = parseFloat(balance.amount) / (10 ** balance.token_info.decimals) console.log(` ${balance.token_info.symbol}: ${amount.toFixed(4)}`) })
// Update wallet UI updateWalletBalances(data.userBalances) }})Advanced Patterns
Section titled “Advanced Patterns”Example 12: Multiple Subscriptions Manager
Section titled “Example 12: Multiple Subscriptions Manager”class SubscriptionManager { constructor(client) { this.client = client this.subscriptions = new Map() }
subscribe(name, query, variables, callback) { // Unsubscribe existing if any this.unsubscribe(name)
const subscription = this.client.subscribe({ query, variables }).subscribe({ next: callback, error: (error) => { console.error(`Subscription ${name} error:`, error) // Attempt reconnection setTimeout(() => { this.subscribe(name, query, variables, callback) }, 5000) } })
this.subscriptions.set(name, subscription) }
unsubscribe(name) { const sub = this.subscriptions.get(name) if (sub) { sub.unsubscribe() this.subscriptions.delete(name) } }
unsubscribeAll() { this.subscriptions.forEach(sub => sub.unsubscribe()) this.subscriptions.clear() }}
// Usageconst manager = new SubscriptionManager(client)
manager.subscribe('trades', WATCH_TRADES, { trader: 'nibi1abc...' }, (data) => { updateTradesUI(data.perpTrades)})
manager.subscribe('prices', WATCH_PRICES, {}, (data) => { updatePricesUI(data.tokenPricesUsd)})
// Cleanup on component unmount// manager.unsubscribeAll()Example 13: Subscription with Reconnection Logic
Section titled “Example 13: Subscription with Reconnection Logic”function createResilientSubscription(query, variables, onData) { let subscription = null let reconnectAttempts = 0 const maxReconnectAttempts = 10
function connect() { subscription = client.subscribe({ query, variables }).subscribe({ next: (data) => { reconnectAttempts = 0 // Reset on successful data onData(data) }, error: (error) => { console.error('Subscription error:', error)
if (reconnectAttempts < maxReconnectAttempts) { reconnectAttempts++ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000) console.log(`Reconnecting in ${delay}ms (attempt ${reconnectAttempts})...`)
setTimeout(connect, delay) } else { console.error('Max reconnection attempts reached') } }, complete: () => { console.log('Subscription completed') } }) }
connect()
return { unsubscribe: () => { if (subscription) { subscription.unsubscribe() } } }}
// Usageconst sub = createResilientSubscription( WATCH_TRADES, { trader: 'nibi1abc...' }, (data) => { console.log('Received trade update:', data.perpTrades) })
// Cleanup// sub.unsubscribe()Best Practices
Section titled “Best Practices”1. Always Clean Up Subscriptions
Section titled “1. Always Clean Up Subscriptions”useEffect(() => { const subscription = client.subscribe({ query: WATCH_TRADES, variables: { trader } }).subscribe({ next: (data) => setTrades(data.perpTrades) })
// Cleanup function return () => { subscription.unsubscribe() }}, [trader])2. Handle Connection States
Section titled “2. Handle Connection States”const [connectionState, setConnectionState] = useState('connecting')
const subscription = client.subscribe({ query: WATCH_PRICES}).subscribe({ next: (data) => { setConnectionState('connected') updatePrices(data) }, error: (error) => { setConnectionState('error') console.error(error) }})
// Show connection status in UIif (connectionState === 'connecting') { return <div>Connecting to real-time feed...</div>}3. Debounce Rapid Updates
Section titled “3. Debounce Rapid Updates”import { debounce } from 'lodash'
const debouncedUpdate = debounce((data) => { updateUI(data)}, 100)
const subscription = client.subscribe({ query: WATCH_PRICES}).subscribe({ next: (data) => { debouncedUpdate(data.tokenPricesUsd) }})4. Combine with Queries for Initial Data
Section titled “4. Combine with Queries for Initial Data”// Fetch initial data with queryconst { data: initialData } = await client.query({ query: GET_TRADES, variables: { trader }})
setTrades(initialData.perp.trades)
// Then subscribe to updatesconst subscription = client.subscribe({ query: WATCH_TRADES, variables: { trader }}).subscribe({ next: (data) => { setTrades(data.perpTrades) }})