Kas tie GraphQL subscriptions ir kodėl jie svarbūs
Jei dirbi su moderniais web aplikacijomis, tikriausiai jau girdėjai apie GraphQL. Bet kai kalbame apie realaus laiko funkcionalumą, dažnai iškyla klausimas – kaip efektyviai gauti duomenis iš serverio be nuolatinio polingo? Čia ir ateina į pagalbą GraphQL subscriptions.
Paprastai tariant, subscriptions leidžia serveriui inicijuoti duomenų siuntimą klientui, kai tik atsitinka tam tikras įvykis. Tai kaip push notifikacijos, tik duomenų lygmenyje. Vietoj to, kad klientas kas sekundę klausinėtų „ar yra naujienų?”, serveris pats praneša „ei, štai tau nauji duomenys”.
Tradicinis REST API veikia pagal request-response modelį. Tu pasiuntei užklausą, gavai atsakymą – viskas. Jei nori naujų duomenų, siųsk naują užklausą. GraphQL subscriptions perverčia šią logiką aukštyn kojomis ir leidžia palaikyti nuolatinį ryšį tarp kliento ir serverio.
Kaip tai veikia po gaubtu
GraphQL subscriptions dažniausiai naudoja WebSocket protokolą. Tai loginė evoliucija, nes WebSockets sukuria dvikryptį komunikacijos kanalą tarp kliento ir serverio. Kai užmezgate subscription, iš esmės sakote serveriui: „klausyk, kai tik pasikeis šie duomenys, praneš man”.
Techniškai tai atrodo taip: klientas siunčia subscription užklausą per WebSocket ryšį. Serveris užregistruoja šią subscription ir laiko ryšį atvirą. Kai serveryje įvyksta atitinkamas event’as (pvz., naujas komentaras, atnaujintas produkto kiekis, nauja žinutė), serveris per tą patį WebSocket kanalą išsiunčia duomenis klientui.
Štai paprastas subscription pavyzdys:
„`graphql
subscription OnNewMessage {
messageAdded(channelId: „123”) {
id
text
author {
name
avatar
}
createdAt
}
}
„`
Kai tik kažkas įrašo naują žinutę į kanalą su ID „123”, visi, kurie yra užsiprenumeravę šį subscription, automatiškai gaus naują žinutę su visais nurodytais laukais. Nereikia jokio polingo, jokių papildomų užklausų.
Kada realiai reikia subscriptions
Dabar svarbus klausimas – kada iš tikrųjų verta naudoti subscriptions? Nes, tiesą sakant, ne visur jų reikia. Jei tavo aplikacija rodo statišką turinį ar duomenys atsinaujina retai, galbūt pakaks paprastos query su cache mechanizmu.
Bet yra scenarijai, kur subscriptions tampa būtinybe:
**Chat aplikacijos** – čia klasika. Niekas nenori kas sekundę siųsti užklausų, ar yra naujų žinučių. Subscriptions leidžia žinutėms atsirasti ekrane iškart, kai jos išsiunčiamos.
**Live feeds ir socialiniai tinklai** – kai rodi realaus laiko aktyvumo srautą, naujus postus ar reakcijas. Facebook, Twitter feed’ai – visa tai veikia panašiais principais.
**Collaborative editing** – Google Docs tipo aplikacijos, kur keli žmonės vienu metu redaguoja tą patį dokumentą. Čia subscriptions būtini, kad matytum kitų pakeitimus realiu laiku.
**Dashboard’ai su live metrikomis** – jei rodi serverio apkrovą, pardavimų statistiką ar bet kokius kitus rodiklius, kurie keičiasi realiu laiku, subscriptions puikiai tinka.
**Gaming ir betting aplikacijos** – kai kursai, rezultatai ar žaidimo būsena keičiasi akimirksniu, vartotojams reikia matyti tai nedelsiant.
Implementavimo niuansai su Apollo Client
Apollo Client yra viena populiariausių bibliotekų dirbant su GraphQL React aplikacijose. Subscriptions setup’as čia gana straightforward, bet yra keletas dalykų, į kuriuos verta atkreipti dėmesį.
Pirma, tau reikės sukonfigūruoti WebSocket link. Tai atrodo maždaug taip:
„`javascript
import { WebSocketLink } from ‘@apollo/client/link/ws’;
import { split, HttpLink } from ‘@apollo/client’;
import { getMainDefinition } from ‘@apollo/client/utilities’;
const httpLink = new HttpLink({
uri: ‘https://api.example.com/graphql’
});
const wsLink = new WebSocketLink({
uri: ‘wss://api.example.com/graphql’,
options: {
reconnect: true,
connectionParams: {
authToken: localStorage.getItem(‘token’),
}
}
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === ‘OperationDefinition’ &&
definition.operation === ‘subscription’
);
},
wsLink,
httpLink,
);
„`
Esmė ta, kad naudojame `split` funkciją, kuri nukreipia subscriptions per WebSocket, o įprastas queries ir mutations per HTTP. Tai efektyvu, nes nereikia palaikyti WebSocket ryšio operacijoms, kurioms jo nereikia.
Komponente subscription naudojimas atrodo labai panašiai kaip ir query:
„`javascript
import { useSubscription } from ‘@apollo/client’;
function ChatMessages({ channelId }) {
const { data, loading } = useSubscription(
MESSAGE_SUBSCRIPTION,
{ variables: { channelId } }
);
if (loading) return
Jungiamasi…
;
return (
)}
);
}
„`
Bet čia slypi viena problema – kiekvieną kartą gavus naują žinutę, `data` objektas atsinaujina, bet ankstesnės žinutės išnyksta. Todėl dažnai reikia kombinuoti query (pradiniam duomenų užkrovimui) su subscription (naujų duomenų gavimui).
Serverio pusė: resolver’iai ir pub-sub sistema
Kalbant apie serverį, subscriptions implementacija šiek tiek skiriasi nuo įprastų resolver’ių. Tau reikės pub-sub (publish-subscribe) mechanizmo. Node.js ekosistemoje populiariausias pasirinkimas – `graphql-subscriptions` paketas su `PubSub` klase.
Paprastas subscription resolver’is atrodo taip:
„`javascript
const { PubSub } = require(‘graphql-subscriptions’);
const pubsub = new PubSub();
const MESSAGE_ADDED = ‘MESSAGE_ADDED’;
const resolvers = {
Subscription: {
messageAdded: {
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([`${MESSAGE_ADDED}_${channelId}`]);
}
}
},
Mutation: {
addMessage: async (_, { channelId, text }, { user }) => {
const message = await createMessage(channelId, text, user);
pubsub.publish(`${MESSAGE_ADDED}_${channelId}`, {
messageAdded: message
});
return message;
}
}
};
„`
Čia matome klasikinį pattern’ą: mutation sukuria naują žinutę ir iškart publikuoja event’ą per pub-sub sistemą. Visi, kurie užsiprenumeravę tą konkretų kanalą, gauna naują žinutę.
Svarbu suprasti, kad `PubSub` iš `graphql-subscriptions` tinka tik development’ui. Production aplinkoje, ypač jei turi kelis serverius, reikia naudoti išorinę pub-sub sistemą kaip Redis. Tam yra `graphql-redis-subscriptions` paketas.
Performance ir skalabilumo klausimai
Subscriptions yra galingi, bet jie kainuoja. Kiekvienas aktyvus subscription reiškia atvirą WebSocket ryšį, o tai – serverio resursai. Jei turi tūkstančius vartotojų su aktyviais subscriptions, tai gali tapti problema.
Pirmas dalykas, kurį reikia apsvarstyti – ar tikrai reikia realaus laiko duomenų? Kartais pakanka atnaujinti duomenis kas 5-10 sekundžių su įprastu polingimu. Tai gerokai paprasčiau ir mažiau apkrauna serverį.
Jei vis tik reikia subscriptions, štai keletas patarimų:
**Naudok Redis ar panašią sistemą pub-sub’ui** – tai leidžia horizontaliai skaluoti serverius. Visi serveriai gali klausytis tų pačių event’ų per Redis.
**Implementuok rate limiting** – apribok, kiek subscriptions vienas vartotojas gali turėti aktyvių vienu metu. Tai apsaugo nuo abuse ir atsitiktinių memory leak’ų kliento pusėje.
**Naudok subscription filters** – vietoj to, kad siųstum visus event’us visiems, filtruok juos serveryje. Pavyzdžiui, jei vartotojas užsiprenumeravęs tik tam tikrus kanalus, nesiųsk jam žinučių iš kitų kanalų.
„`javascript
messageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator([MESSAGE_ADDED]),
(payload, variables) => {
return payload.messageAdded.channelId === variables.channelId;
}
)
}
„`
**Monitorink aktyvius ryšius** – svarbu žinoti, kiek WebSocket ryšių yra aktyvių bet kuriuo metu. Tai padeda nustatyti, kada reikia skaluoti infrastruktūrą.
Alternatyvos ir kada jų ieškoti
GraphQL subscriptions nėra vienintelis būdas gauti realaus laiko duomenis. Priklausomai nuo use case, gali būti geresnių variantų.
**Server-Sent Events (SSE)** – tai paprastesnė alternatyva, kuri veikia per HTTP. Duomenys teka tik viena kryptimi (iš serverio į klientą), bet daugeliu atvejų to pakanka. SSE turi geresnę browser support ir paprastesnę implementaciją.
**Long polling** – senovinis, bet patikimas metodas. Klientas siunčia užklausą, serveris laiko ją atvirą tol, kol atsiranda naujų duomenų arba baigiasi timeout. Tada klientas iškart siunčia naują užklausą. Tai veikia visur ir nereikalauja jokių specialių protokolų.
**WebRTC Data Channels** – jei reikia peer-to-peer komunikacijos su minimaliu latency, WebRTC gali būti geresnis pasirinkimas nei serverio tarpininkaujamas WebSocket.
**Firebase Realtime Database ar Firestore** – jei nenori pats implementuoti visą infrastruktūrą, šios managed paslaugos suteikia realaus laiko funkcionalumą out of the box.
Pasirinkimas priklauso nuo kelių faktorių: kiek duomenų siuntinėsi, kiek vartotojų turėsi, kokia turi būti latency, ar reikia dvikrypčio ryšio, ir kiek resursų gali skirti infrastruktūrai.
Debugging ir tipinės problemos
Dirbant su subscriptions, susidursi su specifinėmis problemomis, kurios neiškyla su įprastomis queries ar mutations.
**WebSocket connection drops** – tai dažna problema. Vartotojas gali prarasti interneto ryšį, pereiti į sleep mode, ar tiesiog naršyklė gali nutraukti ryšį. Apollo Client turi `reconnect: true` opciją, bet kartais reikia papildomos logikos, kad teisingai atstatytum būseną po reconnect.
**Memory leaks** – jei neišsivalai subscriptions, kai komponentas unmount’inasi, gali susikaupti daug aktyvių listener’ių. Visada naudok cleanup funkcijas:
„`javascript
useEffect(() => {
const subscription = subscribeToMore({
document: MESSAGE_SUBSCRIPTION,
variables: { channelId },
updateQuery: (prev, { subscriptionData }) => {
// update logic
}
});
return () => subscription.unsubscribe();
}, [channelId]);
„`
**Authentication issues** – WebSocket ryšiai kartais turi problemų su authentication. Jei naudoji JWT tokens, įsitikink, kad jie perduodami per `connectionParams` ir teisingai validuojami serveryje.
**N+1 problemos** – subscription resolver’iuose irgi gali atsirasti N+1 queries. Naudok DataLoader ar panašius įrankius, kaip ir su įprastomis queries.
Debugging’ui rekomenduoju naudoti Apollo DevTools browser extension. Jis rodo visus aktyvius subscriptions, jų būseną ir gaunamą data. Serverio pusėje, logging yra tavo geriausias draugas – logink kiekvieną subscription start, stop ir kiekvieną publikuojamą event’ą.
Kai realybė susitinka su kodu
GraphQL subscriptions tikrai nėra silver bullet, bet kai jų reikia – jie neįkainojami. Implementavimas gali atrodyti sudėtingas iš pradžių, ypač palyginus su paprastu REST API, bet rezultatas – sklandus, realaus laiko vartotojo patirtis – dažnai būna vertas pastangų.
Svarbiausias patarimas: pradėk paprastai. Nenaudok subscriptions visur, kur tik įmanoma. Identifikuok tikrus use case’us, kur realaus laiko duomenys prideda vertę. Implementuok bazinį sprendimą, testuok jį su reališka apkrova, ir tik tada optimizuok bei plėsk funkcionalumą.
Ir nepamirsk, kad technologijos keičiasi. Šiandien GraphQL subscriptions su WebSocket yra standartas, bet jau dabar matome naujus sprendimus kaip GraphQL over HTTP/2 ar HTTP/3, kurie gali pakeisti kaip galvojame apie realaus laiko duomenis. Svarbu suprasti principus ir būti pasiruošusiam adaptuotis, kai atsiras geresni įrankiai.
