Blog

Websocketi u Next.js-u: rad u stvarnom vremenu na serverless hostingu

· Andrij Tkachenko · 5 min čitanja

Tri kratkotrajne funkcije na poslužitelju, svaka sa štopericom, spojene s oblakom koji s četiri prozora preglednika drži otvorene veze uživo

Dodavanje ažuriranja uživo u Next.js aplikaciju obično dobro počne. U client komponenti otvorite websocket, usmjerite ga na mali poslužitelj i uz next dev sve radi: narudžba se označi kao poslana, pojavi se nova poruka, pomakne se broj ljudi na mreži. Zatim napravite deploy, a veza se prekine, ponovno spoji, opet prekine ili se uopće ne otvori.

Problem je rijetko u kodu. Većina Next.js aplikacija radi na serverless hostingu, a serverless funkcija ne može držati otvorenu vezu. Ovaj članak objašnjava zašto, koliko koštaju uobičajena zaobilazna rješenja i kako Next.js aplikaciji dodati rad u stvarnom vremenu bez pokretanja websocket poslužitelja.

Zašto serverless funkcija ne može držati websocket

Route handler, server action i server komponenta na serverless hostingu rade na isti način. Stigne zahtjev, funkcija se pokrene ili probudi, odgovori, a ubrzo nakon toga bude zamrznuta ili zaustavljena. Sljedeći zahtjev može završiti na sasvim drugoj instanci.

Websocket treba suprotno: jedan proces koji vezu sa svakim preglednikom drži otvorenom dok god je otvorena i stranica, često satima, i koji joj u bilo kojem trenutku može nešto poslati. Serverless funkcija ne živi toliko dugo, ne može doći do veze koju je otvorila druga instanca, a naplaćuje se svaka sekunda dok radi. Zato toliko prvih pokušaja radi u razvoju, gdje sve poslužuje jedan proces koji stalno radi, a u produkciji zakaže.

Uobičajena zaobilazna rješenja

Polling. Stranica svakih nekoliko sekundi pita route handler je li se išta promijenilo. To radi svugdje, ali svako je pitanje jedan poziv funkcije, na većinu njih odgovor je da nema ništa novo, a stranica uvijek kasni nekoliko sekundi. S 1.000 ljudi na stranici koja pita svakih 5 sekundi to je 200 poziva funkcije u sekundi, danonoćno.

Streaming iz route handlera. Route handler može držati odgovor otvorenim i kroz njega slati događaje stranici. To je bliže radu uživo, ali svaki otvoreni stream jedna je funkcija koja još radi, svaka platforma prekida funkcije nakon najduljeg dopuštenog vremena, a stream živi u jednoj instanci, pa događaj poslan iz druge nikad do njega ne stigne.

Prilagođeni poslužitelj (custom server). Next.js može raditi iza vašeg vlastitog Node.js poslužitelja, uz websocket biblioteku. Time aplikacija napušta serverless hosting: održavate poslužitelj koji stalno radi, držite ga u radu kroz svaki deploy, a čim ih imate više od jednog, dijelite svaki događaj među njima, obično preko Redisa, i svaki preglednik držite na poslužitelju koji ga poznaje.

Vlastiti websocket poslužitelj uz aplikaciju. Aplikacija ostaje serverless, a veze drži zaseban poslužitelj. To je najčišće od četiri rješenja, ali je i dalje drugi sustav koji treba održavati u radu: njegov certifikat, njegovo skaliranje, njegov nadzor i način da sazna tko je korisnik, jer ne može čitati sesiju vaše aplikacije.

Ili neka veze drži oblak u stvarnom vremenu

Hostana usluga za rad u stvarnom vremenu u potpunosti preuzima dio koji mora dugo trajati. Preglednici drže otvorene veze s uslugom. Vaš Next.js kod, u server actionu ili route handleru, svaki događaj šalje jednim kratkim HTTP zahtjevom, a upravo to serverless funkcija radi dobro. Usluga ga isporučuje svakoj stranici koja ga treba dobiti, koja god ga je instanca poslala.

Važno je da se uklapa u način na koji Next.js već radi: vaš poslužitelj odlučuje tko se smije pridružiti čemu, na vašim stranicama pretplatu obavljaju client komponente, a na vašem se hostingu ništa ne mijenja.

Next.js na WordPlus Cloudu

S WordPlus Cloudom vaš poslužitelj jednom definira svoje sobe, svaku s pravilom o tome tko joj se smije pridružiti:

lib/realtime.ts
import { createRealtime } from '@wordplus/realtime/server';
import { currentUserId, profileOf } from '@/lib/auth';

export const realtime = createRealtime( {
    site: process.env.WORDPLUS_SITE!,
    secret: process.env.WORDPLUS_SECRET!,
    localKey: process.env.WORDPLUS_LOCAL_KEY!,
    user: () => currentUserId(),
    profile: ( user ) => profileOf( user ),
} );

// The room of an order, order:1042: its customer may join, and the last 20 updates are kept.
realtime.room( 'order:{id}', { history: 20 }, async ( user, { id } ) => user !== null && ( await ownerOf( id ) ) === user );

Jedan catch-all route handler odgovara stranicama kad se traže pridružiti i poslužuje datoteke SDK-a za preglednik:

app/api/realtime/[...path]/route.ts
import { realtime } from '@/lib/realtime';

export const GET = realtime.route;
export const POST = realtime.route;

Layout vaših stranica za prijavljene korisnike renderira RealtimeProvider s realtime.config() za tog korisnika, pa sve stranice ispod njega dijele jednu vezu, koja ostaje i kroz navigacije na strani klijenta. Zatim se client komponenta pridružuje sobi pomoću hooka:

app/(app)/orders/[id]/status.tsx
'use client';

import { useState } from 'react';
import { useRoom } from '@wordplus/realtime/react';

export function Status( { id, initial }: { id: string; initial: string } ) {
    const [ status, setStatus ] = useState( initial );
    useRoom( `order:${ id }`, { updated: ( data: { status: string } ) => setStatus( data.status ) } );
    return <p>{ status }</p>;
}

A server action šalje, nakon što se promjena spremi:

app/(app)/orders/[id]/actions.ts
'use server';

import { realtime } from '@/lib/realtime';

export async function markShipped( id: string ) {
    await saveStatus( id, 'shipped' );
    await realtime.to( `order:${ id }` ).emit( 'updated', { status: 'shipped' } );
}

To je cijeli krug i na serverless hostingu radi takav kakav jest. Osim toga:

  • Prijava ostaje vaša. Korisnik je ID pod kojim ga poznaje vaša aplikacija, pročitan unutar route handlera, pa ondje rade vaša sesija i kolačići.
  • Ništa se ne gubi pri ponovnom spajanju. Soba čuva svoje zadnje događaje, pa stranica koja se vrati dobiva ono što je propustila, redom.
  • Jedna veza za sve kartice. Kartice jednog preglednika dijele jednu vezu, pa se osoba s tri otvorene kartice broji jednom.
  • Zapečaćeno na putu. Ono što vaša aplikacija šalje putuje šifrirano ključevima koje drže samo vaš poslužitelj i vaši korisnici.
  • Više od događaja. Tko je na mreži, pozivi koji zvone u svakoj kartici, grupni pozivi i prijenosi uživo dolaze na istoj vezi, s gotovim zaslonom poziva, chatom i playerom iz Better Messagesa ili s vašim vlastitim komponentama na istom API-ju.

Vodič za Next.js sadrži cijelo podešavanje, a demo uživo je mala trgovina izgrađena na njemu, u kojoj se narudžba, čim kliknete, u drugom prozoru označi kao poslana.

Videopozivi u Next.js aplikaciji, korak po korak: Kako ugraditi videopozive u Next.js aplikaciju.

WordPlus Cloud for Apps ima cijenu prema istodobnim vezama, od 1.000 naviše, uz mjesečnu ili godišnju naplatu. Pogledajte što nudi aplikacijama ili cijene za aplikacije. Pitanja o vašoj integraciji? Pišite nam.