Skip to content

Instantly share code, notes, and snippets.

@geovanisouza92
Last active February 6, 2022 21:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save geovanisouza92/d52cd8a4e2d1c9f0b535f73448ea69b5 to your computer and use it in GitHub Desktop.
Save geovanisouza92/d52cd8a4e2d1c9f0b535f73448ea69b5 to your computer and use it in GitHub Desktop.

Nesse modelo, as atualizações são transmitidas por websockets (socket.io) e focadas nas abas/janelas dos usuários da mesma empresa.

// environments/environment.js
export default {
realtimeEndpoint: 'ws://localhost:3000/'
}
// environments/environment.prod.js
export default {
realtimeEndpoint: 'wss://app-domain.com/'
}
// lib/notes.js
// Nota: essas funções podem ser reutilizadas para prefetch
function fetchNotes() {
return await fetch('/api/notes').then(res => res.json());
}
function fetchNote(id) {
return await fetch(`/api/notes/${id}`).then(res => res.json());
}
// lib/realtime.js
import { useQueryClient } from 'react-query';
import { io } from 'socket.io-client';
import environment from './environments/environment';
function useRealtime() {
const queryClient = useQueryClient();
useEffect(() => {
const socket = io(environment.realtimeEndpoint);
socket.on('invalidate', (key) => {
queryClient.invalidateQueries(key);
// key == ['notes', 'list'] invalida lista de notas
// key == ['notes', 'detail', 5] invalida nota #5
// key == ['notes'] invalida tudo relacionado a notas
});
// Atualização parcial de dados, assumindo que cada item tem um ID.
// Se o item não existir, ele é criado no cache.
socket.on('replace', (key, data) => {
queryClient.setQueryData(key, (oldData) => {
const update = (entity) => entity.id === data.id ? { ...entity, ...data } : entity;
return Array.isArray(oldData) ? oldData.map(update) : update(oldData);
});
});
return () => socket.disconnect();
}, []);
}
// pages/notes/[id].jsx
import { useRouter } from 'next/router';
import { useQuery } from 'react-query';
import { fetchNote } from './lib/notes';
export default function NotePage() {
const { query } = useRouter();
// Vai re-renderizar se a nota for invalidada
const { data } = useNote(query.id);
useRealtime();
// ...
}
function useNote(id) {
const queryClient = useQueryClient();
return useQuery(
['notes', 'detail', id],
() => fetchNote(id),
{
initialData() {
// Try to get the note from the notes list query, if available
// Will continue the fetch() if missing
const state = queryClient.getData(['notes', 'list']);
return state?.data?.find((note) => note.id == id);
}
}
);
}
// pages/notes.jsx
import { useQuery } from 'react-query';
import { fetchNotes } from './lib/notes';
import { useRealtime } from './lib/realtime';
export default function NotesPage() {
// Vai re-renderizar se a lista de notas for atualizada.
const { data } = useNotes();
useRealtime(); // habilita pra página toda
// ...
}
function useNotes() {
return useQuery(['notes', 'list'], fetchNotes);
}
const express = require('express');
const { createServer } = require('http');
const { Server } = require('socket.io');
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer);
// app.get('/api/notes', ...);
// app.get('/api/notes/:id', ...);
app.post('/api/notes', (req, res) => {
// ...
// Descobre a empresa a partir do token e invalida a lista.
// Faz sentido pedir invalidação pq a ordenação pode mudar.
const companyRoom = getTokenIssuer(req);
io.to(companyRoom).emit('invalidate', ['notes', 'list']);
});
app.patch('/api/notes/:id', (req, res) => {
// ...
// Descobre a empresa a partir do token, invalida a nota e
// atualiza parte da lista no lugar.
const companyRoom = getTokenIssuer(req);
io.to(companyRoom).emit('invalidate', ['notes', 'detail', updatedNote.id]);
io.to(companyRoom).emit('replace', ['notes', 'list'], updatedNote);
});
io.on('connection', (socket) => {
const companyRoom = getTokenIssuer(socket.req);
if (companyRoom) socket.join(companyRoom);
});
function getTokenIssuer(req) {
// Auth pode ser capturada de cookies, com a vantagem de ser incluída
// em todas as requisições automaticamente pelo navegador.
const { authorization } = req.headers;
if (!authorization) return null;
const [, token] = authorization.split(' ');
const { iss } = jwt.verify(token, environment.jwtSecret);
return iss; // cc:123
}
httpServer.listen(3000);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment