Preskoči na glavno vsebino

Sinhronizacija stanja med browser tabi: Najtežji problem, ki smo ga rešili (ki ga niste vedeli, da potrebujete)

· 5 min branja
Osama Alghanmi
Soustanovitelj in tehnični vodja

Sinhronizacija stanja med browser tabi: Orbitalni pristop

Kako ohranjamo stanje popolnoma sinhronizirano med več tabi brez WebSocketov ali kompleksne backend logike.

Ste imeli kdaj dva taba iste aplikacije odprta in sta izgubila sinhronizacijo? Mi smo to popravili na nivoju frameworka.

Problem več tabov

Uporabljate aplikacijo. Odprete drugi tab. Naredite spremembe v prvem tabu. Drugi tab se ne posodobi.

Pogosti scenariji:

  • Tab 1: Urejanje dokumenta
  • Tab 2: Isti dokument, stara različica
  • Rezultat: Konflikt ob shranjevanju

Ali:

  • Tab 1: Opravilo zaključeno
  • Tab 2: Opravilo še vedno prikazano kot nedokončano
  • Rezultat: Zmeda, podvojene akcije

Večina aplikacij to ignorira. Almadar to reši.

Zakaj je to težko

Sinhronizacija stanja med tabi zahteva:

  1. Detekcijo sprememb — Vedeti, kdaj se stanje spremeni
  2. Transport — Pošiljati spremembe med tabi
  3. Reševanje konfliktov — Obravnavati sočasna urejanja
  4. Verzioniranje — Slediti, katero stanje je najnovejše
  5. Performanco — Ne preobremeniti z posodobitvami

Kako to deluje v Almadarju

Arhitekturni pregled

Tab 1 (Klient A)          Transport           Tab 2 (Klient B)
│ │ │
│ notifyStateChange() │ │
│──────────┐ │ │
│ ▼ │ │
│ StateSyncManager │ WebSocket/SSE │
│ │ │◄──────────────────►│
│ │ stateChange │ │
│ └──────────────►│ │
│ │ broadcast │
│ │────────────────────►│
│ │ │
│ │ receiveRemoteChange()
│ │ │
│ │◄───────────────────┘
│ │ │
│ conflict? │ │
│◄─────────────────────────┤ │

StateSyncManager

// Internal: Almadar's state synchronization system
const syncManager = new StateSyncManager({
clientId: 'browser-tab-1',
conflictStrategy: 'last_write_wins',
throttleInterval: 100, // ms
maxRetries: 3,
});

Korak 1: Obvesti o lokalnih spremembah

Ko se stanje spremeni v Tabu 1:

// Uporabnik ustvari checkpoint
syncManager.notifyStateChange('checkpoint_created', threadId, {
checkpointId: 'chk_123',
step: 5,
timestamp: Date.now(),
});

To ustvari StateChangeEvent:

interface StateChangeEvent {
type: StateChangeType;
threadId: string;
userId?: string;
timestamp: number;
payload: Record<string, unknown>;
version: VersionVector;
sourceClientId: string;
}

Korak 2: Verzijski vektorji

Vsaka sprememba ima verzijski vektor za detekcijo konfliktov:

interface VersionVector {
timestamp: number; // Logični čas
sequence: number; // Monotoni števec
nodeId: string; // Identifikator klienta
}

Primer:

{
timestamp: 1709312400000,
sequence: 42,
nodeId: 'browser-tab-1'
}

Korak 3: Transportni nivo

Server obravnava transport preko WebSocket:

// Server-side WebSocket setup
io.on('connection', (socket) => {
const userId = socket.data.user.uid;
const clientId = socket.handshake.auth.clientId;

// Pridruži se sobi uporabnika za ciljane posodobitve
socket.join(`user:${userId}`);

// Posreduj spremembe stanja drugim tabom
socket.on('stateChange', (event) => {
socket.to(`user:${userId}`).emit('remoteChange', event);
});
});

Korak 4: Prejmi oddaljene spremembe

Tab 2 prejme spremembo:

syncManager.on('remoteChange', (event) => {
// Uporabi spremembo na lokalno stanje
updateLocalState(event.type, event.payload);

// Posodobi UI
refreshUI();
});

Korak 5: Reševanje konfliktov

Če oba taba urejata hkrati:

// Tab 1 spremeni status ob T1
syncManager.notifyStateChange('status_changed', threadId, {
status: 'approved'
});

// Tab 2 spremeni status ob T2 (nekoliko kasneje)
syncManager.notifyStateChange('status_changed', threadId, {
status: 'rejected'
});

Strategije:

StrategijaKako delujeNajboljše za
last_write_winsZmaga najnovejši timestampVečina primerov
mergeZdruži spremembe, če je mogočeSodelovalno urejanje
manualOpozori uporabnika za rešitevKritični podatki
const syncManager = new StateSyncManager({
conflictStrategy: 'last_write_wins',
});

// Obravnavaj konflikte
syncManager.on('conflictDetected', (conflicts, incoming) => {
// Pokaži UI za reševanje konfliktov
showConflictDialog(conflicts);
});

Popoln primer

Client-Side Setup

import { useEffect, useRef } from 'react';
import { io } from 'socket.io-client';

// Internal: getStateSyncManager() vrne singleton sync manager

export function useStateSync(threadId: string | null) {
const syncManagerRef = useRef(getStateSyncManager());

useEffect(() => {
if (!threadId) return;

// Poveži se s sync serverjem
const socket = io('/state-sync', {
auth: {
token: getAuthToken(),
clientId: syncManagerRef.current.getConfig().clientId,
},
});

// Prejmi spremembe iz drugih tabov
socket.on('remoteChange', (event) => {
syncManagerRef.current.receiveRemoteChange(event);
});

// Obravnavaj konflikte
syncManagerRef.current.on('conflictDetected', (conflicts) => {
console.warn('State conflict:', conflicts);
// Pokaži UI za ročno reševanje
});

// Pošlji lokalne spremembe
syncManagerRef.current.on('syncRequired', (changes) => {
changes.forEach(change => {
socket.emit('stateChange', change);
});
});

return () => {
socket.disconnect();
};
}, [threadId]);

return syncManagerRef.current;
}

React Component Usage

function TaskBoard({ threadId }: { threadId: string }) {
const syncManager = useStateSync(threadId);
const [tasks, setTasks] = useState([]);

const moveTask = (taskId: string, newStatus: string) => {
// Posodobi lokalno stanje
setTasks(prev => prev.map(t =>
t.id === taskId ? { ...t, status: newStatus } : t
));

// Obvesti druge tabe
syncManager.notifyStateChange('task_moved', threadId, {
taskId,
newStatus,
});
};

// Poslušaj za oddaljene spremembe
useEffect(() => {
const handleRemoteChange = (event: StateChangeEvent) => {
if (event.type === 'task_moved') {
setTasks(prev => prev.map(t =>
t.id === event.payload.taskId
? { ...t, status: event.payload.newStatus }
: t
));
}
};

syncManager.on('remoteChange', handleRemoteChange);
return () => {
syncManager.off('remoteChange', handleRemoteChange);
};
}, [syncManager]);

return (
<div>
{tasks.map(task => (
<TaskCard
key={task.id}
task={task}
onMove={moveTask}
/>
));
}
</div>
);
}

Tipi sprememb stanja

Almadar sinhronizira te tipe dogodkov:

type StateChangeType =
| 'checkpoint_created'
| 'checkpoint_updated'
| 'session_started'
| 'session_ended'
| 'tool_executed'
| 'memory_updated'
| 'interrupt_triggered'
| 'interrupt_resolved';

Vsak se preslika v specifično UI posodobitev.

Primer iz resničnega sveta: AI par programiranje

Scenarij: Programirate z AI-jem čez dva taba.

Tab 1: Gledate AI-ja pri delu Tab 2: Pregledujete dokumentacijo

Brez sinhronizacije:

  • Tab 1: AI ustvari checkpoint
  • Tab 2: Še vedno prikazuje staro stanje
  • Preklopite na Tab 2, naredite spremembe
  • Konflikt ko se vrnete v Tab 1

Z Almadar sinhronizacijo:

  • Tab 1: AI ustvari checkpoint
  • Tab 2: Samodejno se posodobi za prikaz novega checkpointa
  • Oba taba sta sinhronizirana
  • Ni konfliktov

Optimizacije performanse

1. Throttling

Ne sinhroniziraj vsakega pritiska tipke:

const syncManager = new StateSyncManager({
throttleInterval: 100, // Združi spremembe znotraj 100ms
});

2. Debouncing

Za visoko-frekvenčne posodobitve:

// Internal: debounce utility for high-frequency sync events
const debouncedNotify = debounceSync(syncManager, 500);

// Klicano ob vsakem pritisku tipke
debouncedNotify('document_edited', threadId, { content });
// Dejansko pošlje po 500ms nedejavnosti

3. Selektivna sinhronizacija

Sinhroniziraj samo kar je pomembno:

// Sinhroniziraj to (pomembno stanje)
syncManager.notifyStateChange('checkpoint_created', threadId, payload);

// Ne sinhroniziraj tega (prehodno UI stanje)
// (samo lokalni React state)

Primerjava: Pred vs Po

Pred sinhronizacijo stanja

AkcijaTab 1Tab 2
ZačetnoPrikaže Nalogo APrikaže Nalogo A
Uredi v Tabu 1Naloga A posodobljenaNaloga A (stara)
Uredi v Tabu 2-Naloga A (konflikt!)
RezultatKonfliktKonflikt

Po sinhronizaciji stanja

AkcijaTab 1Tab 2
ZačetnoPrikaže Nalogo APrikaže Nalogo A
Uredi v Tabu 1Naloga A posodobljenaNaloga A samodejno posodobljena
Uredi v Tabu 2Samodejno posodobljenoNaloga B posodobljena
RezultatSinhroniziranoSinhronizirano

Primerjava iz resničnega sveta: Google Docs

Google Docs je rešil to za dokumente:

  • Več ljudi ureja
  • Spremembe se pojavijo v realnem času
  • Konflikti se rešijo samodejno

Almadar prinaša to v katero koli stanje aplikacije:

  • Checkpoints
  • Napredek seje
  • Posodobitve spomina
  • UI stanje

Spoznanje

Sinhronizacija več tabov je težka. Večina aplikacij to ignorira. Uporabniki trpijo.

Almadarjev StateSyncManager:

  • ✅ Deluje na nivoju frameworka
  • ✅ Pametno obravnava konflikte
  • ✅ Optimiziran za performanco
  • ✅ Transparenten za razvijalce

Ker vaši uporabniki ne bi smeli razmišljati o tem, v katerem tabu so.

Več o Sinhronizaciji stanja.

Nedavne objave