Move toju-app into own its folder
This commit is contained in:
183
toju-app/src/app/store/messages/messages.helpers.ts
Normal file
183
toju-app/src/app/store/messages/messages.helpers.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* 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/message.rules';
|
||||
import type { InventoryItem } from '../../domains/chat/domain/message-sync.rules';
|
||||
|
||||
// Re-export domain logic so existing callers keep working
|
||||
export {
|
||||
getMessageTimestamp,
|
||||
getLatestTimestamp,
|
||||
normaliseDeletedMessage,
|
||||
canEditMessage
|
||||
} from '../../domains/chat/domain/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/message-sync.rules';
|
||||
export type { InventoryItem } from '../../domains/chat/domain/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);
|
||||
|
||||
const reactions = await db.getReactionsForMessage(msg.id);
|
||||
|
||||
return reactions.length > 0 ? { ...msg,
|
||||
reactions } : msg;
|
||||
}
|
||||
|
||||
/** Hydrates an array of messages with their reactions. */
|
||||
export async function hydrateMessages(
|
||||
messages: Message[],
|
||||
db: DatabaseService
|
||||
): Promise<Message[]> {
|
||||
return Promise.all(messages.map((msg) => hydrateMessage(msg, db)));
|
||||
}
|
||||
|
||||
/** 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user