feat: implement Redis Streams support with stream checkpoints and update history

- Added Redis Streams operations to the message bus interface and implementation.
- Introduced StreamCheckpoint model to track last processed stream entry per document.
- Implemented UpsertStreamCheckpoint and GetStreamCheckpoint methods in the Postgres store.
- Created document_update_history table for storing update payloads for recovery and replay.
- Developed update persist worker to handle Redis Stream updates and persist them to Postgres.
- Enhanced Docker Compose configuration for Redis with persistence.
- Updated frontend API to support fetching document state with optional share token.
- Added connection stability monitoring in the Yjs document hook.
This commit is contained in:
M1ngdaXie
2026-03-08 17:13:42 -07:00
parent f319e8ec75
commit 50822600ad
22 changed files with 1371 additions and 78 deletions

View File

@@ -53,8 +53,11 @@ export const documentsApi = {
},
// Get document Yjs state
getState: async (id: string): Promise<Uint8Array> => {
const response = await authFetch(`${API_BASE_URL}/documents/${id}/state`);
getState: async (id: string, shareToken?: string): Promise<Uint8Array> => {
const url = shareToken
? `${API_BASE_URL}/documents/${id}/state?share=${shareToken}`
: `${API_BASE_URL}/documents/${id}/state`;
const response = await authFetch(url);
if (!response.ok) throw new Error("Failed to fetch document state");
const arrayBuffer = await response.arrayBuffer();
return new Uint8Array(arrayBuffer);
@@ -167,4 +170,4 @@ export const versionsApi = {
if (!response.ok) throw new Error('Failed to restore version');
return response.json();
},
};
};

View File

@@ -157,10 +157,34 @@ export const useYjsDocument = (documentId: string, shareToken?: string) => {
setSynced(true);
});
// Connection stability monitoring with reconnection limits
let reconnectCount = 0;
const maxReconnects = 10;
yjsProviders.websocketProvider.on(
"status",
(event: { status: string }) => {
console.log("WebSocket status:", event.status);
if (event.status === "disconnected") {
reconnectCount++;
if (reconnectCount >= maxReconnects) {
console.error(
"Max reconnection attempts reached. Please refresh the page."
);
// Could optionally show a user notification here
} else {
console.log(
`Reconnection attempt ${reconnectCount}/${maxReconnects}`
);
}
} else if (event.status === "connected") {
// Reset counter on successful connection
if (reconnectCount > 0) {
console.log("Reconnected successfully, resetting counter");
}
reconnectCount = 0;
}
}
);

View File

@@ -30,7 +30,7 @@ export const createYjsDocument = async (
// Load initial state from database BEFORE connecting providers
try {
const state = await documentsApi.getState(documentId);
const state = await documentsApi.getState(documentId, shareToken);
if (state && state.length > 0) {
Y.applyUpdate(ydoc, state);
console.log('✓ Loaded document state from database');
@@ -51,7 +51,10 @@ export const createYjsDocument = async (
wsUrl,
documentId,
ydoc,
{ params: wsParams }
{
params: wsParams,
maxBackoffTime: 10000, // Max 10s between reconnect attempts
}
);
// Awareness for cursors and presence