Subscription adalah jenis operasi GraphQL yang ketiga, setelah query dan mutation. Query bertanya sekali lalu selesai. Subscription bilang "kabari aku tiap kali ini berubah", lalu server mendorong (push) data lewat koneksi yang tetap terbuka. Bayangin Budi yang menunggu Bumi Manusia datang lagi. Cara polling: Budi menelepon toko tiap 5 menit, "sudah ada belum?", padahal jawabannya hampir selalu "belum". Cara subscription: Budi titip nomor HP ke kasir, dan kasir mengirim pesan begitu stoknya masuk.
🧪 Tebakan cepat: Budi subscribe ke perubahan stok Bumi Manusia (
bookId: 2). Lalu Sari, staf toko, mengubah stok Laskar Pelangi jadi 11. Apakah Budi menerima pesan? Jawabannya ada di bagian "Filter per subscriber".
Schema dan resolver subscribe
Di schema, subscription punya root type sendiri:
type Subscription {
stockChanged(bookId: ID!): Book!
}
Budi mengirim operasi ini. Satu subscription cuma boleh memilih satu field teratas; dua field ditolak dengan Anonymous Subscription must select only one top level field.
subscription PantauStok($bookId: ID!) {
stockChanged(bookId: $bookId) {
title
status
}
}
Di server, field subscription punya resolver subscribe yang mengembalikan aliran event (sebuah AsyncIterable). Event-nya dikirim oleh mutation lewat PubSub. Dengan paket graphql-subscriptions versi 3:
import { PubSub, withFilter } from "graphql-subscriptions";
const pubsub = new PubSub();
const resolvers = {
Mutation: {
updateStock: (_, { id, stock }, context) => {
// ...cek staf dan ubah stok seperti di pelajaran sebelumnya
pubsub.publish("STOCK_CHANGED", { stockChanged: { ...book } });
return book;
},
},
Subscription: {
stockChanged: {
subscribe: withFilter(
() => pubsub.asyncIterableIterator("STOCK_CHANGED"),
(payload, args) => String(payload.stockChanged.id) === args.bookId,
),
},
},
};
publish("STOCK_CHANGED", ...)menyiarkan event ke semua yang mendengarkan topik itu.asyncIterableIterator(di versi 2 namanyaasyncIterator) memberi aliran event untuk satu subscriber.- Tiap event dijalankan seperti query kecil, dengan event itu sebagai root value.
stockChangeddibaca daripayload.stockChanged, lalutitledanstatusdiisi seperti biasa. Jadi Budi menerima satu response per event:{ "data": { "stockChanged": { "title": "Bumi Manusia", "status": "IN_STOCK" } } }. - Event berisi salinan
{ ...book }, bukan baris aslinya. Event dijalankan sesaat kemudian; kalau isinya baris asli dan stok berubah dua kali dengan cepat, kedua pesan bisa sama-sama membawa stok terakhir.
Filter per subscriber
Semua perubahan stok masuk ke topik yang sama. withFilter menyaring per subscriber: fungsi kedua menerima event dan argument subscriber itu, dan cuma event yang lolos yang diteruskan. Budi subscribe dengan bookId: "2", sedangkan event Laskar Pelangi punya id: 1. Filternya mengembalikan false, jadi Budi gak menerima apa-apa. Itu jawaban tebakannya. Begitu Sari mengubah stok Bumi Manusia jadi 7, filternya lolos dan Budi dapat status: "IN_STOCK".
Transport: WebSocket dengan graphql-ws
Spesifikasi GraphQL gak mengatur cara mengirim event. Yang paling umum adalah WebSocket dengan protokol dari paket graphql-ws (versi 6.3.0). Ini percakapan aslinya, direkam dari server graphql-ws sungguhan:
| Arah | Pesan |
|---|---|
| Budi → server | {"type":"connection_init","payload":{"authorization":"Bearer token-budi"}} |
| server → Budi | {"type":"connection_ack"} |
| Budi → server | {"id":"1","type":"subscribe","payload":{"query":"subscription PantauStok(...) {...}","variables":{"bookId":"2"}}} |
| server → Budi | {"id":"1","type":"next","payload":{"data":{"stockChanged":{"title":"Bumi Manusia","status":"IN_STOCK"}}}} |
| Budi → server | {"id":"1","type":"complete"} |
- WebSocket dibuka dengan subprotocol
graphql-transport-ws. Pesan pertama wajibconnection_init; server menjawabconnection_ack. subscribememulai satu operasi denganid. Tiap event datang sebagainextdenganidyang sama.completedari client berarti berhenti; dari server berarti operasinya selesai. Error validasi datang sebagai{"type":"error"}, danping/pongmenjaga koneksi tetap hidup.
Di sisi client:
import { createClient } from "graphql-ws";
const client = createClient({
url: "wss://api.tokobuku.test/graphql",
connectionParams: { authorization: "Bearer token-budi" },
});
const stop = client.subscribe(
{ query: PANTAU_STOK, variables: { bookId: "2" } },
{ next: (response) => console.log(response.data), error: console.error, complete: () => {} },
);
Kamu mungkin ketemu library lama subscriptions-transport-ws. Rilis terakhirnya 0.11.0 (November 2021), dan paketnya sudah ditandai deprecated di npm dengan pesan untuk pindah ke graphql-ws. Protokolnya beda dan gak saling cocok. Membingungkannya, subprotocol library lama itu bernama graphql-ws.
Auth di connection_init
WebSocket di browser gak bisa diberi header Authorization. Karena itu token dikirim di payload connection_init, lalu server memeriksanya sekali per koneksi:
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/use/ws";
const wsServer = new WebSocketServer({ port: 4000, path: "/graphql" });
useServer(
{
schema,
// Sekali per koneksi. false: koneksi ditutup dengan kode 4403 Forbidden.
onConnect: async (ctx) => Boolean(await userFromBearer(ctx.connectionParams?.authorization)),
// Untuk tiap operasi di koneksi itu.
context: async (ctx) => ({ user: await userFromBearer(ctx.connectionParams?.authorization) }),
},
wsServer,
);
- Token palsu:
onConnectmengembalikanfalse, koneksi ditutup dengan kode4403dan alasanForbidden. - Mengirim
subscribesebelumconnection_ack: ditutup dengan4401 Unauthorized. - Aturan per field tetap jalan di tiap event. Kalau Budi (pelanggan) ikut meminta
stock, tiap pesannextmembawastock: nulldan errorFORBIDDEN, sama seperti di pelajaran "Authentication & Authorization".
Alternatif: SSE dengan graphql-sse
Server-Sent Events (SSE) memakai satu request HTTP biasa yang jawabannya terus mengalir. Paket graphql-sse (versi 2.6.2) memakainya untuk subscription. Client mengirim POST /graphql/stream dengan Accept: text/event-stream, dan server menjawab 200 dengan content-type: text/event-stream; charset=utf-8, lalu tiap event:
event: next
data: {"data":{"stockChanged":{"title":"Bumi Manusia","status":"IN_STOCK"}}}
Karena ini HTTP biasa, token cukup di header Authorization, dan proxy atau load balancer yang paham HTTP gak perlu pengaturan khusus. Arahnya cuma server ke client; client yang ingin mengirim sesuatu memakai request HTTP biasa. Untuk subscription yang memang cuma mendengar, itu cukup.
Subscription atau polling?
Polling tiap 5 detik berarti 12 request per menit untuk tiap user, walaupun stoknya gak berubah. Subscription cuma mengirim pesan saat ada event. Tapi subscription punya biaya: tiap user memegang satu koneksi terbuka, dan PubSub di memori cuma bekerja dalam satu proses server. Kalau servernya lebih dari satu, event harus lewat sesuatu yang dipakai bersama, misalnya Redis PubSub. Pakai subscription untuk perubahan kecil yang sering dan harus cepat sampai (chat, stok, notifikasi). Untuk data yang jarang berubah, polling atau refetch biasa lebih sederhana.
⚠️ Jebakan yang sering ketemu
- PubSub di memori saat server lebih dari satu. Mutation di server A gak terdengar subscriber di server B. Pakai PubSub bersama (Redis dan sejenisnya).
- Lupa filter. Tanpa
withFilter, semua subscriber menerima perubahan semua buku. - Token di URL WebSocket. URL sering tercatat di log. Kirim lewat
connectionParams. - Mencampur dua protokol. Client
graphql-wsgak bisa bicara dengan serversubscriptions-transport-ws, begitu juga sebaliknya.
TL;DR: Subscription adalah operasi untuk menerima data real-time: type Subscription { stockChanged(bookId: ID!): Book! }, satu field teratas per operasi. Resolver subscribe mengembalikan aliran event (misalnya pubsub.asyncIterableIterator("STOCK_CHANGED")), mutation memanggil pubsub.publish, dan tiap event dijalankan seperti query kecil, jadi satu response per event. withFilter membuat Budi cuma menerima perubahan buku yang ia pantau. Transport yang umum adalah WebSocket dengan graphql-ws: connection_init, connection_ack, subscribe, next, complete, dengan token di payload connection_init (token palsu: 4403 Forbidden). subscriptions-transport-ws gak dirawat lagi (rilis terakhir 2021) dan ditandai deprecated di npm. SSE dengan graphql-sse adalah alternatif lewat HTTP biasa. Subscription lebih hemat dari polling untuk data yang sering berubah, tapi butuh koneksi terbuka dan PubSub bersama saat server lebih dari satu.