A page that shows a list goes stale the moment someone else changes it. Polling every few seconds costs calls and still lags. The v3 change stream, GET /public/v3/stream/changes, sends every create, update and delete in your own v3 data as Server-Sent Events, about a second after the commit. Your token needs the stream:read scope.
The events
| Event | Data | What to do |
|---|---|---|
ready |
{} |
The stream is open. Read the data you show now; changes from here on come as events. |
change |
{"resource":"posts","action":"updated","id":"...","version":2,"href":"/public/v3/posts/..."} |
Read the row at href, or drop it from the page when action is deleted. |
reset |
{} |
Your data changed as a whole, or the stream lost its place. Read the data you show again. |
A comment line : heartbeat comes every 25 seconds, so a proxy keeps the connection open. Each event has an id. Keep the last one: it resumes the stream after a drop, and the events you missed in the last 5 minutes come first.
In a browser: EventSource with a ticket
A browser EventSource cannot send an Authorization header, and an API token must never go in a URL, where it ends up in logs and history. So first get a stream ticket with your token. A ticket works once, within 60 seconds, and only on the stream.
const API = 'https://gorest.co.in/public/v3';
const token = 'YOUR_TOKEN'; // a token with only stream:read, kept in memory
let lastId = null;
let wait = 2000;
async function connect() {
let res;
try {
res = await fetch(`${API}/stream/tickets`, {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
});
} catch {
return again(); // offline: try later
}
if (res.status === 401 || res.status === 403) {
console.error('the token is wrong, revoked or lacks stream:read'); // stop
return;
}
if (!res.ok) return again(); // 429 or 5xx: try later
const { ticket } = await res.json();
const url = new URL(`${API}/stream/changes`);
url.searchParams.set('ticket', ticket);
if (lastId) url.searchParams.set('last-event-id', lastId);
const es = new EventSource(url);
const keep = (e) => { lastId = e.lastEventId || lastId; wait = 2000; };
es.addEventListener('ready', (e) => { keep(e); reloadAll(); });
es.addEventListener('reset', (e) => { keep(e); reloadAll(); });
es.addEventListener('change', (e) => { keep(e); applyChange(JSON.parse(e.data)); });
// The stream ends after 30 minutes, on a server restart or a drop. The
// browser would try the same URL again, but the ticket is used up, so
// close it and connect again with a new ticket.
es.onerror = () => { es.close(); again(); };
}
// again waits 2 s, doubling up to a minute while connects keep failing,
// plus a random part so many pages do not come back in the same second.
function again() {
setTimeout(connect, wait + Math.random() * 3000);
wait = Math.min(wait * 2, 60000);
}
connect();reloadAll() reads the list you show, and applyChange(c) reads c.href or removes c.id. An event can come twice after a reconnect, so make both safe to run again: replace a row by its id, never append.
With fetch, or outside a browser
Where you can send a header, no ticket is needed. Read the body as a stream and split it on blank lines:
const res = await fetch(`${API}/stream/changes`, {
headers: { Authorization: `Bearer ${token}`, ...(lastId && { 'Last-Event-ID': lastId }) },
});
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
let buf = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break; // connect again, with Last-Event-ID
buf += value;
let end;
while ((end = buf.indexOf('\n\n')) >= 0) {
const frame = buf.slice(0, end);
buf = buf.slice(end + 2);
const field = (name) => frame.split('\n').find((l) => l.startsWith(name + ': '))?.slice(name.length + 2);
if (field('id')) lastId = field('id');
if (field('event')) console.log(field('event'), field('data'));
}
}From a terminal: curl -N -H "Authorization: Bearer $TOKEN" https://gorest.co.in/public/v3/stream/changes. Then change something in another terminal and watch the event arrive.
Keep the token safe
- Anyone who opens the page can read a token in its JavaScript. Use a token with only the
stream:readscope for a page like this, and revoke it on the tokens page when you are done. - A ticket goes in the URL, so it can show in proxy and server logs. That is why it works only once and only for 60 seconds. Your API token itself must never go in a URL.
Limits
- At most 3 open streams per token and 10 per account on each server; one more answers 429.
- A stream ends after 30 minutes, at the token's end, or when the token is revoked. Connect again after the
retrytime the stream sent. - Each ticket call and each connect spends one call of your token's rate limit.
- A ticket is used up by any connect, also one that is refused (a 429 when you have too many streams, for one). Get a new ticket before every connect, as the code above does.
Keep building
Test loading skeletons with simulated latency
Use /http/delay/<ms> to make GoRest take seconds to respond, so you can verify your skeleton screens render without flicker.
Test 401, 422, 429 and 500 handlers
Force any status code with /http/status/<code> to make sure your error UI never shows blank screens or eats stack traces.
Build a paginated user list in React
A working component that fetches a page, renders rows, and uses the v3 cursor to render Previous/Next correctly.