Files
ST-Bionic-Memory-Ecology/graph/graph-persistence.js
2026-04-10 17:00:51 +08:00

982 lines
29 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ST-BME: 图谱持久化常量与纯工具函数
// 不依赖 index.js 模块级可变状态currentGraph / graphPersistenceState 等)
import { deserializeGraph, getGraphStats, serializeGraph } from "./graph.js";
import { normalizeGraphRuntimeState } from "../runtime/runtime-state.js";
// ═══════════════════════════════════════════════════════════
// 常量
// ═══════════════════════════════════════════════════════════
export const MODULE_NAME = "st_bme";
export const GRAPH_METADATA_KEY = "st_bme_graph";
export const GRAPH_COMMIT_MARKER_KEY = "st_bme_commit_marker";
export const GRAPH_CHAT_STATE_NAMESPACE = `${MODULE_NAME}_graph_state`;
export const GRAPH_CHAT_STATE_VERSION = 1;
export const GRAPH_CHAT_STATE_MAX_OPERATIONS = 4000;
export const GRAPH_PERSISTENCE_META_KEY = "__stBmePersistence";
export const GRAPH_LOAD_STATES = Object.freeze({
NO_CHAT: "no-chat",
LOADING: "loading",
LOADED: "loaded",
SHADOW_RESTORED: "shadow-restored",
EMPTY_CONFIRMED: "empty-confirmed",
BLOCKED: "blocked",
});
export const GRAPH_LOAD_PENDING_CHAT_ID = "__pending_chat__";
export const GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX = `${MODULE_NAME}:graph-shadow:`;
export const GRAPH_IDENTITY_ALIAS_STORAGE_KEY = `${MODULE_NAME}:chat-identity-aliases`;
export const GRAPH_STARTUP_RECONCILE_DELAYS_MS = [150, 600, 1800, 4000];
// ═══════════════════════════════════════════════════════════
// 纯工具
// ═══════════════════════════════════════════════════════════
export function cloneRuntimeDebugValue(value, fallback = null) {
if (value == null) {
return fallback;
}
try {
return JSON.parse(JSON.stringify(value));
} catch {
return fallback ?? value;
}
}
export function createLocalIntegritySlug() {
const nativeUuid = globalThis.crypto?.randomUUID?.();
if (nativeUuid) return nativeUuid;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
const random = Math.floor(Math.random() * 16);
const value = char === "x" ? random : (random & 0x3) | 0x8;
return value.toString(16);
});
}
export const GRAPH_PERSISTENCE_SESSION_ID = createLocalIntegritySlug();
function normalizeIdentityValue(value) {
return String(value ?? "").trim();
}
function getLocalStorageSafe() {
const storage = globalThis.localStorage;
if (
!storage ||
typeof storage.getItem !== "function" ||
typeof storage.setItem !== "function"
) {
return null;
}
return storage;
}
function getSessionStorageSafe() {
const storage = globalThis.sessionStorage;
if (!storage || typeof storage.getItem !== "function") {
return null;
}
return storage;
}
function listStorageKeys(storage) {
if (!storage) return [];
if (typeof storage.length === "number" && typeof storage.key === "function") {
const keys = [];
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index);
if (typeof key === "string" && key) {
keys.push(key);
}
}
return keys;
}
if (storage.__store instanceof Map) {
return Array.from(storage.__store.keys()).map((key) => String(key));
}
return [];
}
function readGraphIdentityAliasRegistryRaw() {
const storage = getLocalStorageSafe();
if (!storage) {
return {
byIntegrity: {},
};
}
try {
const raw = storage.getItem(GRAPH_IDENTITY_ALIAS_STORAGE_KEY);
if (!raw) {
return {
byIntegrity: {},
};
}
const parsed = JSON.parse(raw);
const byIntegrity =
parsed?.byIntegrity &&
typeof parsed.byIntegrity === "object" &&
!Array.isArray(parsed.byIntegrity)
? parsed.byIntegrity
: {};
return {
byIntegrity,
};
} catch {
return {
byIntegrity: {},
};
}
}
function writeGraphIdentityAliasRegistryRaw(registry = null) {
const storage = getLocalStorageSafe();
if (!storage) return false;
try {
storage.setItem(
GRAPH_IDENTITY_ALIAS_STORAGE_KEY,
JSON.stringify({
byIntegrity:
registry?.byIntegrity &&
typeof registry.byIntegrity === "object" &&
!Array.isArray(registry.byIntegrity)
? registry.byIntegrity
: {},
}),
);
return true;
} catch {
return false;
}
}
function normalizeGraphIdentityAliasEntry(entry = {}, integrity = "") {
const normalizedIntegrity = normalizeIdentityValue(integrity || entry.integrity);
const normalizedPersistenceChatId = normalizeIdentityValue(
entry.persistenceChatId || normalizedIntegrity,
);
const normalizedHostChatIds = Array.from(
new Set(
(Array.isArray(entry.hostChatIds) ? entry.hostChatIds : [])
.map((value) => normalizeIdentityValue(value))
.filter(Boolean),
),
).slice(-16);
return {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId || normalizedIntegrity,
hostChatIds: normalizedHostChatIds,
updatedAt: String(entry.updatedAt || ""),
};
}
export function rememberGraphIdentityAlias({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
if (!normalizedIntegrity) return null;
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
const normalizedPersistenceChatId = normalizeIdentityValue(
persistenceChatId || normalizedIntegrity,
);
const registry = readGraphIdentityAliasRegistryRaw();
const existingEntry = normalizeGraphIdentityAliasEntry(
registry.byIntegrity?.[normalizedIntegrity] || {},
normalizedIntegrity,
);
const hostChatIds = Array.from(
new Set(
[normalizedHostChatId, ...existingEntry.hostChatIds].filter(Boolean),
),
).slice(-16);
const nextEntry = {
integrity: normalizedIntegrity,
persistenceChatId: normalizedPersistenceChatId || normalizedIntegrity,
hostChatIds,
updatedAt: new Date().toISOString(),
};
registry.byIntegrity[normalizedIntegrity] = nextEntry;
writeGraphIdentityAliasRegistryRaw(registry);
return nextEntry;
}
export function resolveGraphIdentityAliasByHostChatId(hostChatId = "") {
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
if (!normalizedHostChatId) return "";
const registry = readGraphIdentityAliasRegistryRaw();
let bestEntry = null;
for (const [integrity, value] of Object.entries(registry.byIntegrity || {})) {
const entry = normalizeGraphIdentityAliasEntry(value, integrity);
if (!entry.hostChatIds.includes(normalizedHostChatId)) {
continue;
}
if (!bestEntry) {
bestEntry = entry;
continue;
}
if (String(entry.updatedAt || "") > String(bestEntry.updatedAt || "")) {
bestEntry = entry;
}
}
return normalizeIdentityValue(bestEntry?.persistenceChatId || "");
}
export function getGraphIdentityAliasCandidates({
integrity = "",
hostChatId = "",
persistenceChatId = "",
} = {}) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
const normalizedHostChatId = normalizeIdentityValue(hostChatId);
const normalizedPersistenceChatId = normalizeIdentityValue(persistenceChatId);
const registry = readGraphIdentityAliasRegistryRaw();
const candidates = [];
const seen = new Set();
const pushCandidate = (value) => {
const normalized = normalizeIdentityValue(value);
if (!normalized || seen.has(normalized)) return;
seen.add(normalized);
candidates.push(normalized);
};
if (normalizedIntegrity) {
const entry = normalizeGraphIdentityAliasEntry(
registry.byIntegrity?.[normalizedIntegrity] || {},
normalizedIntegrity,
);
pushCandidate(entry.persistenceChatId);
for (const value of entry.hostChatIds) {
pushCandidate(value);
}
} else if (normalizedHostChatId) {
pushCandidate(resolveGraphIdentityAliasByHostChatId(normalizedHostChatId));
}
pushCandidate(normalizedHostChatId);
pushCandidate(normalizedPersistenceChatId);
return candidates;
}
function normalizeShadowSnapshotPayload(snapshot = null) {
if (!snapshot || typeof snapshot !== "object") {
return null;
}
const serializedGraph = String(snapshot.serializedGraph || "");
const chatId = normalizeIdentityValue(snapshot.chatId);
if (!chatId || !serializedGraph) {
return null;
}
return {
chatId,
revision: Number.isFinite(snapshot.revision) ? snapshot.revision : 0,
serializedGraph,
updatedAt: String(snapshot.updatedAt || ""),
reason: String(snapshot.reason || ""),
integrity: normalizeIdentityValue(snapshot.integrity),
persistedChatId: normalizeIdentityValue(snapshot.persistedChatId),
debugReason: String(snapshot.debugReason || snapshot.reason || ""),
};
}
// ═══════════════════════════════════════════════════════════
// 图谱持久化元数据
// ═══════════════════════════════════════════════════════════
/**
* @param {object} graph
* @returns {object|null}
*/
export function getGraphPersistenceMeta(graph) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const meta = graph[GRAPH_PERSISTENCE_META_KEY];
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
return null;
}
return meta;
}
/**
* @param {object} graph
* @returns {number}
*/
export function getGraphPersistedRevision(graph) {
const revision = Number(getGraphPersistenceMeta(graph)?.revision);
return Number.isFinite(revision) && revision > 0 ? revision : 0;
}
/**
* @param {object} graph
* @param {object} opts
* @param {number} [opts.revision]
* @param {string} [opts.reason]
* @param {string} [opts.chatId]
* @param {string} [opts.integrity]
*/
export function stampGraphPersistenceMeta(
graph,
{ revision = 0, reason = "", chatId = "", integrity = "" } = {},
) {
if (!graph || typeof graph !== "object" || Array.isArray(graph)) {
return null;
}
const existingMeta = getGraphPersistenceMeta(graph) || {};
const nextMeta = {
...existingMeta,
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
updatedAt: new Date().toISOString(),
sessionId: GRAPH_PERSISTENCE_SESSION_ID,
reason: String(reason || ""),
chatId: String(chatId || existingMeta.chatId || ""),
integrity: String(integrity || existingMeta.integrity || ""),
};
graph[GRAPH_PERSISTENCE_META_KEY] = nextMeta;
return nextMeta;
}
// ═══════════════════════════════════════════════════════════
// 聊天元数据
// ═══════════════════════════════════════════════════════════
export function writeChatMetadataPatch(context, patch = {}) {
if (!context) return false;
if (typeof context.updateChatMetadata === "function") {
context.updateChatMetadata(patch);
return true;
}
if (
!context.chatMetadata ||
typeof context.chatMetadata !== "object" ||
Array.isArray(context.chatMetadata)
) {
context.chatMetadata = {};
}
Object.assign(context.chatMetadata, patch || {});
return true;
}
export function canUseGraphChatState(context = null) {
return (
!!context &&
typeof context.getChatState === "function" &&
typeof context.updateChatState === "function"
);
}
export function normalizeGraphChatStateSnapshot(snapshot = null) {
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) {
return null;
}
const version = Number(snapshot.version);
const revision = Number(snapshot.revision);
const serializedGraph = String(snapshot.serializedGraph || "");
const storageTier = String(snapshot.storageTier || "chat-state");
const chatId = normalizeIdentityValue(snapshot.chatId);
const integrity = normalizeIdentityValue(snapshot.integrity);
const commitMarker = normalizeGraphCommitMarker(snapshot.commitMarker);
if (!serializedGraph) {
return null;
}
return {
version: Number.isFinite(version) && version > 0 ? version : GRAPH_CHAT_STATE_VERSION,
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
serializedGraph,
persistedAt: String(snapshot.persistedAt || ""),
updatedAt: String(snapshot.updatedAt || snapshot.persistedAt || ""),
reason: String(snapshot.reason || ""),
storageTier,
chatId,
integrity,
commitMarker,
};
}
export function buildGraphChatStateSnapshot(
graph,
{
revision = 0,
storageTier = "chat-state",
accepted = true,
reason = "",
persistedAt = "",
updatedAt = "",
chatId = "",
integrity = "",
lastProcessedAssistantFloor = null,
extractionCount = null,
} = {},
) {
if (!graph) {
return null;
}
const commitMarker = buildGraphCommitMarker(graph, {
revision,
storageTier,
accepted,
reason,
persistedAt,
chatId,
integrity,
lastProcessedAssistantFloor,
extractionCount,
});
return normalizeGraphChatStateSnapshot({
version: GRAPH_CHAT_STATE_VERSION,
revision,
serializedGraph: serializeGraph(graph),
persistedAt: String(persistedAt || new Date().toISOString()),
updatedAt: String(updatedAt || persistedAt || new Date().toISOString()),
reason: String(reason || ""),
storageTier: String(storageTier || "chat-state"),
chatId,
integrity,
commitMarker,
});
}
export async function readGraphChatStateSnapshot(
context = null,
{ namespace = GRAPH_CHAT_STATE_NAMESPACE } = {},
) {
if (!canUseGraphChatState(context)) {
return null;
}
try {
const payload = await context.getChatState(namespace);
return normalizeGraphChatStateSnapshot(payload);
} catch (error) {
console.warn("[ST-BME] 读取聊天侧车图谱失败:", error);
return null;
}
}
export async function writeGraphChatStateSnapshot(
context = null,
graph = null,
{
namespace = GRAPH_CHAT_STATE_NAMESPACE,
revision = 0,
storageTier = "chat-state",
accepted = true,
reason = "",
chatId = "",
integrity = "",
lastProcessedAssistantFloor = null,
extractionCount = null,
maxOperations = GRAPH_CHAT_STATE_MAX_OPERATIONS,
} = {},
) {
if (!canUseGraphChatState(context) || !graph) {
return {
ok: false,
updated: false,
snapshot: null,
reason: "chat-state-unavailable",
};
}
const snapshot = buildGraphChatStateSnapshot(graph, {
revision,
storageTier,
accepted,
reason,
chatId,
integrity,
lastProcessedAssistantFloor,
extractionCount,
});
if (!snapshot) {
return {
ok: false,
updated: false,
snapshot: null,
reason: "chat-state-build-failed",
};
}
try {
const result = await context.updateChatState(
namespace,
() => snapshot,
{
maxOperations,
asyncDiff: false,
maxRetries: 1,
},
);
return {
ok: result?.ok === true,
updated: result?.updated !== false,
snapshot,
reason:
result?.ok === true
? result?.updated === false
? "chat-state-noop"
: "chat-state-saved"
: "chat-state-save-failed",
};
} catch (error) {
console.warn("[ST-BME] 写入聊天侧车图谱失败:", error);
return {
ok: false,
updated: false,
snapshot,
reason: "chat-state-save-failed",
error,
};
}
}
export function normalizeGraphCommitMarker(marker = null) {
if (!marker || typeof marker !== "object" || Array.isArray(marker)) {
return null;
}
const revision = Number(marker.revision);
const lastProcessedAssistantFloor = Number(marker.lastProcessedAssistantFloor);
const extractionCount = Number(marker.extractionCount);
const nodeCount = Number(marker.nodeCount);
const edgeCount = Number(marker.edgeCount);
const archivedCount = Number(marker.archivedCount);
return {
revision: Number.isFinite(revision) && revision > 0 ? revision : 0,
lastProcessedAssistantFloor:
Number.isFinite(lastProcessedAssistantFloor)
? Math.floor(lastProcessedAssistantFloor)
: -1,
extractionCount:
Number.isFinite(extractionCount) && extractionCount >= 0
? Math.floor(extractionCount)
: 0,
nodeCount:
Number.isFinite(nodeCount) && nodeCount >= 0 ? Math.floor(nodeCount) : 0,
edgeCount:
Number.isFinite(edgeCount) && edgeCount >= 0 ? Math.floor(edgeCount) : 0,
archivedCount:
Number.isFinite(archivedCount) && archivedCount >= 0
? Math.floor(archivedCount)
: 0,
persistedAt: String(marker.persistedAt || ""),
storageTier: String(marker.storageTier || "none"),
accepted: marker.accepted === true,
reason: String(marker.reason || ""),
chatId: normalizeIdentityValue(marker.chatId),
integrity: normalizeIdentityValue(marker.integrity),
};
}
export function buildGraphCommitMarker(
graph,
{
revision = 0,
storageTier = "none",
accepted = false,
reason = "",
persistedAt = "",
chatId = "",
integrity = "",
lastProcessedAssistantFloor = null,
extractionCount = null,
} = {},
) {
const stats = graph ? getGraphStats(graph) : null;
const historyState = graph?.historyState || {};
const hasExplicitLastProcessedFloor =
lastProcessedAssistantFloor !== null &&
lastProcessedAssistantFloor !== undefined &&
lastProcessedAssistantFloor !== "";
const hasExplicitExtractionCount =
extractionCount !== null &&
extractionCount !== undefined &&
extractionCount !== "";
return normalizeGraphCommitMarker({
revision,
lastProcessedAssistantFloor:
hasExplicitLastProcessedFloor &&
Number.isFinite(Number(lastProcessedAssistantFloor))
? Number(lastProcessedAssistantFloor)
: Number.isFinite(Number(historyState.lastProcessedAssistantFloor))
? Number(historyState.lastProcessedAssistantFloor)
: Number.isFinite(Number(stats?.lastProcessedSeq))
? Number(stats.lastProcessedSeq)
: -1,
extractionCount:
hasExplicitExtractionCount &&
Number.isFinite(Number(extractionCount))
? Number(extractionCount)
: Number.isFinite(Number(historyState.extractionCount))
? Number(historyState.extractionCount)
: 0,
nodeCount: Number(stats?.activeNodes || 0),
edgeCount: Number(stats?.totalEdges || 0),
archivedCount: Number(stats?.archivedNodes || 0),
persistedAt: String(persistedAt || new Date().toISOString()),
storageTier: String(storageTier || "none"),
accepted: accepted === true,
reason: String(reason || ""),
chatId,
integrity,
});
}
export function readGraphCommitMarker(context = null) {
const rawMarker =
context?.chatMetadata &&
typeof context.chatMetadata === "object" &&
!Array.isArray(context.chatMetadata)
? context.chatMetadata[GRAPH_COMMIT_MARKER_KEY]
: null;
const marker = normalizeGraphCommitMarker(rawMarker);
return marker?.revision ? marker : null;
}
export function getAcceptedCommitMarkerRevision(marker = null) {
const normalizedMarker = normalizeGraphCommitMarker(marker);
return normalizedMarker?.accepted === true
? Number(normalizedMarker.revision || 0)
: 0;
}
export function detectIndexedDbSnapshotCommitMarkerMismatch(
snapshot = null,
marker = null,
) {
const normalizedMarker = normalizeGraphCommitMarker(marker);
if (!normalizedMarker || normalizedMarker.accepted !== true) {
return {
mismatched: false,
reason: "",
markerRevision: 0,
snapshotRevision: Number.isFinite(Number(snapshot?.meta?.revision))
? Number(snapshot.meta.revision)
: 0,
};
}
const snapshotRevision = Number.isFinite(Number(snapshot?.meta?.revision))
? Number(snapshot.meta.revision)
: 0;
const markerRevision = Number(normalizedMarker.revision || 0);
if (markerRevision <= 0 || snapshotRevision >= markerRevision) {
return {
mismatched: false,
reason: "",
markerRevision,
snapshotRevision,
};
}
return {
mismatched: true,
reason: "persist-mismatch:indexeddb-behind-commit-marker",
markerRevision,
snapshotRevision,
marker: normalizedMarker,
};
}
// ═══════════════════════════════════════════════════════════
// Shadow Snapshot会话存储
// ═══════════════════════════════════════════════════════════
export function getGraphShadowSnapshotStorageKey(chatId = "") {
const normalizedChatId = String(chatId || "").trim();
if (!normalizedChatId) return "";
return `${GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX}${encodeURIComponent(normalizedChatId)}`;
}
export function readGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return null;
try {
const raw = getSessionStorageSafe()?.getItem(storageKey);
if (!raw) return null;
const snapshot = normalizeShadowSnapshotPayload(JSON.parse(raw));
if (!snapshot || snapshot.chatId !== String(chatId || "")) {
return null;
}
return snapshot;
} catch {
return null;
}
}
export function findGraphShadowSnapshotByIntegrity(
integrity = "",
{ excludeChatIds = [] } = {},
) {
const normalizedIntegrity = normalizeIdentityValue(integrity);
if (!normalizedIntegrity) return null;
const storage = getSessionStorageSafe();
if (!storage) return null;
const excludedChatIds = new Set(
(Array.isArray(excludeChatIds) ? excludeChatIds : [])
.map((value) => normalizeIdentityValue(value))
.filter(Boolean),
);
let bestSnapshot = null;
for (const key of listStorageKeys(storage)) {
if (!String(key || "").startsWith(GRAPH_SHADOW_SNAPSHOT_STORAGE_PREFIX)) {
continue;
}
try {
const snapshot = normalizeShadowSnapshotPayload(
JSON.parse(storage.getItem(key)),
);
if (!snapshot || snapshot.integrity !== normalizedIntegrity) {
continue;
}
if (excludedChatIds.has(snapshot.chatId)) {
continue;
}
const bestRevision = Number(bestSnapshot?.revision || 0);
const nextRevision = Number(snapshot.revision || 0);
if (!bestSnapshot || nextRevision > bestRevision) {
bestSnapshot = snapshot;
continue;
}
if (
nextRevision === bestRevision &&
String(snapshot.updatedAt || "") > String(bestSnapshot.updatedAt || "")
) {
bestSnapshot = snapshot;
}
} catch {
// ignore broken shadow snapshot payloads
}
}
return bestSnapshot;
}
/**
* @param {string} chatId
* @param {object} graph
* @param {object} [opts]
* @param {number} [opts.revision]
* @param {string} [opts.reason]
*/
export function writeGraphShadowSnapshot(
chatId,
graph,
{ revision = 0, reason = "", integrity = "", debugReason = "" } = {},
) {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey || !graph) return false;
try {
const serializedGraph = serializeGraph(graph);
const persistedMeta = getGraphPersistenceMeta(graph) || {};
getSessionStorageSafe()?.setItem(
storageKey,
JSON.stringify({
chatId: String(chatId || ""),
revision: Number.isFinite(revision) ? revision : 0,
serializedGraph,
updatedAt: new Date().toISOString(),
reason: String(reason || ""),
integrity: String(integrity || persistedMeta.integrity || ""),
persistedChatId: String(persistedMeta.chatId || ""),
debugReason: String(debugReason || reason || ""),
}),
);
return true;
} catch (error) {
console.warn("[ST-BME] 写入会话图谱临时快照失败:", error);
return false;
}
}
export function removeGraphShadowSnapshot(chatId = "") {
const storageKey = getGraphShadowSnapshotStorageKey(chatId);
if (!storageKey) return false;
try {
getSessionStorageSafe()?.removeItem(storageKey);
return true;
} catch {
return false;
}
}
// ═══════════════════════════════════════════════════════════
// 图谱克隆 / 比较
// ═══════════════════════════════════════════════════════════
export function cloneGraphForPersistence(graph, chatId = "") {
return normalizeGraphRuntimeState(
deserializeGraph(serializeGraph(graph)),
chatId,
);
}
export function shouldPreferShadowSnapshotOverOfficial(
officialGraph,
shadowSnapshot,
) {
if (!shadowSnapshot) {
return {
prefer: false,
reason: "shadow-missing",
resultCode: "shadow.missing",
};
}
const shadowRevision = Number(shadowSnapshot.revision || 0);
const officialRevision = getGraphPersistedRevision(officialGraph);
const officialMeta = getGraphPersistenceMeta(officialGraph) || {};
const normalizedOfficialChatId = String(officialMeta.chatId || "").trim();
const normalizedShadowChatId = String(shadowSnapshot.chatId || "").trim();
const normalizedShadowPersistedChatId = String(
shadowSnapshot.persistedChatId || "",
).trim();
const officialIntegrity = String(officialMeta.integrity || "").trim();
const shadowIntegrity = String(shadowSnapshot.integrity || "").trim();
if (shadowRevision <= 0) {
return {
prefer: false,
reason: "shadow-revision-invalid",
resultCode: "shadow.reject.revision-invalid",
shadowRevision,
officialRevision,
};
}
if (
normalizedOfficialChatId &&
normalizedShadowPersistedChatId &&
normalizedOfficialChatId !== normalizedShadowPersistedChatId
) {
return {
prefer: false,
reason: "shadow-persisted-chat-mismatch",
resultCode: "shadow.reject.persisted-chat-mismatch",
shadowRevision,
officialRevision,
officialChatId: normalizedOfficialChatId,
shadowPersistedChatId: normalizedShadowPersistedChatId,
};
}
if (
normalizedOfficialChatId &&
normalizedShadowChatId &&
normalizedOfficialChatId !== normalizedShadowChatId
) {
return {
prefer: false,
reason: "shadow-chat-mismatch",
resultCode: "shadow.reject.chat-mismatch",
shadowRevision,
officialRevision,
officialChatId: normalizedOfficialChatId,
shadowChatId: normalizedShadowChatId,
};
}
if (
officialIntegrity &&
shadowIntegrity &&
officialIntegrity !== shadowIntegrity
) {
return {
prefer: false,
reason: "shadow-integrity-mismatch",
resultCode: "shadow.reject.integrity-mismatch",
shadowRevision,
officialRevision,
officialIntegrity,
shadowIntegrity,
};
}
if (
normalizedShadowPersistedChatId &&
normalizedShadowChatId &&
normalizedShadowPersistedChatId !== normalizedShadowChatId
) {
return {
prefer: false,
reason: "shadow-self-chat-mismatch",
resultCode: "shadow.reject.self-chat-mismatch",
shadowRevision,
officialRevision,
shadowChatId: normalizedShadowChatId,
shadowPersistedChatId: normalizedShadowPersistedChatId,
};
}
if (normalizedShadowPersistedChatId && !normalizedOfficialChatId) {
return {
prefer: false,
reason: "shadow-persisted-chat-without-official-chat",
resultCode: "shadow.reject.persisted-chat-without-official-chat",
shadowRevision,
officialRevision,
shadowPersistedChatId: normalizedShadowPersistedChatId,
};
}
if (shadowIntegrity && !officialIntegrity) {
return {
prefer: false,
reason: "shadow-integrity-without-official-integrity",
resultCode: "shadow.reject.integrity-without-official-integrity",
shadowRevision,
officialRevision,
shadowIntegrity,
};
}
return {
prefer: shadowRevision > 0 && shadowRevision > officialRevision,
reason:
shadowRevision > officialRevision
? "shadow-newer-than-official"
: "shadow-not-newer-than-official",
resultCode:
shadowRevision > officialRevision
? "shadow.accept.newer-than-official"
: "shadow.keep.official-not-older",
shadowRevision,
officialRevision,
};
}