feat(chat-switch): in-memory message cache helper
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
import { afterEach, describe, expect, it } from 'vitest';
|
||||||
|
import type { DecryptedMessage } from '@chat-app/shared/chat';
|
||||||
|
|
||||||
|
import {
|
||||||
|
__resetForTests,
|
||||||
|
getCachedMessages,
|
||||||
|
hasCachedMessages,
|
||||||
|
setCachedMessages,
|
||||||
|
} from './messageMemoryCache';
|
||||||
|
|
||||||
|
function msg(id: string): DecryptedMessage {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
conversationId: 'conv-1',
|
||||||
|
senderId: 'sender-1',
|
||||||
|
senderDeviceId: null,
|
||||||
|
replyToId: null,
|
||||||
|
editedAt: null,
|
||||||
|
deletedAt: null,
|
||||||
|
createdAt: '2026-05-17T00:00:00Z',
|
||||||
|
plaintext: 'hi ' + id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('messageMemoryCache', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
__resetForTests();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty array when nothing is cached', () => {
|
||||||
|
expect(getCachedMessages('unknown')).toEqual([]);
|
||||||
|
expect(hasCachedMessages('unknown')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores and returns messages per conversation', () => {
|
||||||
|
setCachedMessages('a', [msg('m1'), msg('m2')]);
|
||||||
|
expect(getCachedMessages('a').map((m) => m.id)).toEqual(['m1', 'm2']);
|
||||||
|
expect(hasCachedMessages('a')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isolates conversations', () => {
|
||||||
|
setCachedMessages('a', [msg('m1')]);
|
||||||
|
setCachedMessages('b', [msg('m9')]);
|
||||||
|
expect(getCachedMessages('a').map((m) => m.id)).toEqual(['m1']);
|
||||||
|
expect(getCachedMessages('b').map((m) => m.id)).toEqual(['m9']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites prior cache when set again', () => {
|
||||||
|
setCachedMessages('a', [msg('m1')]);
|
||||||
|
setCachedMessages('a', [msg('m1'), msg('m2')]);
|
||||||
|
expect(getCachedMessages('a').map((m) => m.id)).toEqual(['m1', 'm2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an explicit empty list as "cached"', () => {
|
||||||
|
// A conversation that genuinely has zero messages should still be
|
||||||
|
// flagged as cached so the hook skips the loading spinner on re-entry.
|
||||||
|
setCachedMessages('a', []);
|
||||||
|
expect(hasCachedMessages('a')).toBe(true);
|
||||||
|
expect(getCachedMessages('a')).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// In-memory cache of the most-recently-rendered messages for each
|
||||||
|
// conversation. Survives React component unmount/remount (used by
|
||||||
|
// `useConversationMessages` to initialize state synchronously when
|
||||||
|
// ConversationPage is remounted on chat switch). Session-scoped — lost
|
||||||
|
// on full app reload. The SQLite cache (`messageCache.ts`) is still the
|
||||||
|
// source of truth for cross-session persistence; this layer just shaves
|
||||||
|
// off the round-trip-to-disk spinner flash.
|
||||||
|
//
|
||||||
|
// Two-tier semantics:
|
||||||
|
// * `hasCachedMessages(id)` returns true even for a known-empty chat
|
||||||
|
// so the hook can suppress the loading spinner on re-entry.
|
||||||
|
// * `getCachedMessages(id)` returns a defensive copy so callers can't
|
||||||
|
// mutate the cached array.
|
||||||
|
|
||||||
|
import type { DecryptedMessage } from '@chat-app/shared/chat';
|
||||||
|
|
||||||
|
const cache = new Map<string, DecryptedMessage[]>();
|
||||||
|
|
||||||
|
export function getCachedMessages(conversationId: string): DecryptedMessage[] {
|
||||||
|
const stored = cache.get(conversationId);
|
||||||
|
return stored ? stored.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasCachedMessages(conversationId: string): boolean {
|
||||||
|
return cache.has(conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCachedMessages(
|
||||||
|
conversationId: string,
|
||||||
|
messages: DecryptedMessage[],
|
||||||
|
): void {
|
||||||
|
cache.set(conversationId, messages.slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function __resetForTests(): void {
|
||||||
|
cache.clear();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user