From 61dbca8dc22bf6f709501d8568538dacec40a1c1 Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 13 Jan 2026 20:55:21 +0800 Subject: [PATCH] fix: prevent browser cache from serving stale memo data (#5470) This fixes a critical data loss issue where users editing the same memo on multiple devices would overwrite each other's changes due to aggressive browser caching, particularly in Chromium-based browsers and PWAs. Changes: - Backend: Add Cache-Control headers to all API responses to prevent browser HTTP caching - Frontend: Force fresh fetch from server when opening memo editor by invalidating React Query cache - Frontend: Reduce memo query staleTime from 60s to 10s for better collaborative editing support Fixes #5470 --- server/router/api/v1/connect_interceptors.go | 14 +++++++++++++- web/src/components/MemoEditor/hooks/useMemoInit.ts | 9 ++++++++- web/src/hooks/useMemoQueries.ts | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/server/router/api/v1/connect_interceptors.go b/server/router/api/v1/connect_interceptors.go index 03eb35de4..348c89279 100644 --- a/server/router/api/v1/connect_interceptors.go +++ b/server/router/api/v1/connect_interceptors.go @@ -50,7 +50,19 @@ func (*MetadataInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc // Set metadata in context so services can use metadata.FromIncomingContext() ctx = metadata.NewIncomingContext(ctx, md) - return next(ctx, req) + + // Execute the request + resp, err := next(ctx, req) + + // Prevent browser caching of API responses to avoid stale data issues + // See: https://github.com/usememos/memos/issues/5470 + if resp != nil { + resp.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + resp.Header().Set("Pragma", "no-cache") + resp.Header().Set("Expires", "0") + } + + return resp, err } } diff --git a/web/src/components/MemoEditor/hooks/useMemoInit.ts b/web/src/components/MemoEditor/hooks/useMemoInit.ts index 7586c0e62..6cdc0f8f6 100644 --- a/web/src/components/MemoEditor/hooks/useMemoInit.ts +++ b/web/src/components/MemoEditor/hooks/useMemoInit.ts @@ -1,4 +1,6 @@ +import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; +import { memoKeys } from "@/hooks/useMemoQueries"; import type { Visibility } from "@/types/proto/api/v1/memo_service_pb"; import type { EditorRefActions } from "../Editor"; import { cacheService, memoService } from "../services"; @@ -13,6 +15,7 @@ export const useMemoInit = ( defaultVisibility?: Visibility, ) => { const { actions, dispatch } = useEditorContext(); + const queryClient = useQueryClient(); const initializedRef = useRef(false); useEffect(() => { @@ -24,6 +27,10 @@ export const useMemoInit = ( try { if (memoName) { + // Force refetch from server to prevent stale data issues + // See: https://github.com/usememos/memos/issues/5470 + await queryClient.invalidateQueries({ queryKey: memoKeys.detail(memoName) }); + // Load existing memo const loadedState = await memoService.load(memoName); dispatch( @@ -58,5 +65,5 @@ export const useMemoInit = ( }; init(); - }, [memoName, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, editorRef]); + }, [memoName, cacheKey, username, autoFocus, defaultVisibility, actions, dispatch, editorRef, queryClient]); }; diff --git a/web/src/hooks/useMemoQueries.ts b/web/src/hooks/useMemoQueries.ts index bb61426ed..6cb107e5a 100644 --- a/web/src/hooks/useMemoQueries.ts +++ b/web/src/hooks/useMemoQueries.ts @@ -53,7 +53,7 @@ export function useMemo(name: string, options?: { enabled?: boolean }) { return memo; }, enabled: options?.enabled ?? true, - staleTime: 1000 * 60, // 1 minute - memos can be edited frequently + staleTime: 1000 * 10, // 10 seconds - reduced to prevent stale data in collaborative editing }); }