Files
Toju/toju-app/src/app/store/messages/messages.helpers.ts
2026-05-18 19:38:08 +02:00

181 lines
5.0 KiB
TypeScript

/**
* Message store helpers - delegates pure domain logic to `domains/chat/domain/`
* and provides DB-dependent hydration/merge operations at the application level.
*/
import { Message } from '../../shared-kernel';
import { DatabaseService } from '../../infrastructure/persistence';
import { getMessageTimestamp, normaliseDeletedMessage } from '../../domains/chat/domain/rules/message.rules';
import type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';
// Re-export domain logic so existing callers keep working
export {
getMessageTimestamp,
getLatestTimestamp,
normaliseDeletedMessage,
canEditMessage
} from '../../domains/chat/domain/rules/message.rules';
export {
INVENTORY_LIMIT,
CHUNK_SIZE,
SYNC_POLL_FAST_MS,
SYNC_POLL_SLOW_MS,
SYNC_TIMEOUT_MS,
FULL_SYNC_LIMIT,
chunkArray,
findMissingIds
} from '../../domains/chat/domain/rules/message-sync.rules';
export type { InventoryItem } from '../../domains/chat/domain/rules/message-sync.rules';
/** Hydrates a single message with its reactions from the database. */
export async function hydrateMessage(
msg: Message,
_db: DatabaseService
): Promise<Message> {
if (msg.isDeleted)
return normaliseDeletedMessage(msg);
return msg;
}
/** Hydrates an array of messages with their reactions. */
export async function hydrateMessages(
messages: Message[],
_db: DatabaseService
): Promise<Message[]> {
return messages.map((msg) => msg.isDeleted ? normaliseDeletedMessage(msg) : msg);
}
/** Builds a sync inventory item from a message and its reaction count. */
export async function buildInventoryItem(
msg: Message,
db: DatabaseService,
attachmentCountOverride?: number
): Promise<InventoryItem> {
if (msg.isDeleted) {
return {
id: msg.id,
ts: getMessageTimestamp(msg),
rc: 0,
ac: 0
};
}
const reactions = await db.getReactionsForMessage(msg.id);
const attachments =
attachmentCountOverride === undefined
? await db.getAttachmentsForMessage(msg.id)
: [];
return { id: msg.id,
ts: getMessageTimestamp(msg),
rc: reactions.length,
ac: attachmentCountOverride ?? attachments.length };
}
/** Builds a local map of `{timestamp, reactionCount, attachmentCount}` keyed by message ID. */
export async function buildLocalInventoryMap(
messages: Message[],
db: DatabaseService,
attachmentCountOverrides?: ReadonlyMap<string, number>
): Promise<Map<string, { ts: number; rc: number; ac: number }>> {
const map = new Map<string, { ts: number; rc: number; ac: number }>();
await Promise.all(
messages.map(async (msg) => {
if (msg.isDeleted) {
map.set(msg.id, {
ts: getMessageTimestamp(msg),
rc: 0,
ac: 0
});
return;
}
const reactions = await db.getReactionsForMessage(msg.id);
const attachmentCountOverride = attachmentCountOverrides?.get(msg.id);
const attachments =
attachmentCountOverride === undefined
? await db.getAttachmentsForMessage(msg.id)
: [];
map.set(msg.id, { ts: getMessageTimestamp(msg),
rc: reactions.length,
ac: attachmentCountOverride ?? attachments.length });
})
);
return map;
}
/** Result of merging an incoming message into the local database. */
export interface MergeResult {
message: Message;
changed: boolean;
}
/**
* Merges an incoming message into the local database.
* Handles message upsert and reaction deduplication, then returns
* the fully hydrated message alongside a `changed` flag.
*/
export async function mergeIncomingMessage(
incoming: Message,
db: DatabaseService
): Promise<MergeResult> {
const existing = await db.getMessageById(incoming.id);
const existingTs = existing ? getMessageTimestamp(existing) : -1;
const incomingTs = getMessageTimestamp(incoming);
const isDeletedStateNewer =
!!existing &&
incomingTs === existingTs &&
incoming.isDeleted &&
!existing.isDeleted;
const isNewer = !existing || incomingTs > existingTs || isDeletedStateNewer;
if (isNewer) {
await db.saveMessage(incoming);
}
// Persist incoming reactions (deduped by the DB layer)
const incomingReactions = incoming.isDeleted ? [] : incoming.reactions ?? [];
for (const reaction of incomingReactions) {
await db.saveReaction(reaction);
}
const changed = isNewer || incomingReactions.length > 0;
if (changed) {
const baseMessage = isNewer ? incoming : existing;
if (!baseMessage) {
return { message: normaliseDeletedMessage(incoming),
changed };
}
if (baseMessage.isDeleted) {
return {
message: normaliseDeletedMessage(baseMessage),
changed
};
}
const reactions = await db.getReactionsForMessage(incoming.id);
return {
message: { ...baseMessage,
reactions },
changed
};
}
if (!existing) {
return { message: normaliseDeletedMessage(incoming),
changed: false };
}
return { message: normaliseDeletedMessage(existing),
changed: false };
}