mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-05-15 22:30:38 +08:00
perf: optimize persist/load P1 hot paths
This commit is contained in:
181
sync/bme-db.js
181
sync/bme-db.js
@@ -646,6 +646,26 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
!Array.isArray(options.baseSnapshot)
|
||||
? options.baseSnapshot
|
||||
: {};
|
||||
const shouldCollectDiagnostics = typeof options?.onDiagnostics === "function";
|
||||
const snapshotStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const snapshotDiagnostics = shouldCollectDiagnostics
|
||||
? {
|
||||
nodeCount: 0,
|
||||
edgeCount: 0,
|
||||
tombstoneCount: 0,
|
||||
reusedNodeCount: 0,
|
||||
reusedEdgeCount: 0,
|
||||
reusedTombstoneCount: 0,
|
||||
clonedNodeCount: 0,
|
||||
clonedEdgeCount: 0,
|
||||
clonedTombstoneCount: 0,
|
||||
nodesMs: 0,
|
||||
edgesMs: 0,
|
||||
tombstonesMs: 0,
|
||||
stateMs: 0,
|
||||
metaMs: 0,
|
||||
}
|
||||
: null;
|
||||
const baseSnapshot = sanitizeSnapshot(baseSnapshotInput);
|
||||
const baseSnapshotView = normalizePersistSnapshotView(baseSnapshotInput);
|
||||
const nowMs = normalizeTimestamp(options.nowMs, Date.now());
|
||||
@@ -674,6 +694,7 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
baseSnapshotView.tombstones,
|
||||
);
|
||||
|
||||
const nodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const nodes = toArray(runtimeGraph?.nodes)
|
||||
.map((node) => {
|
||||
if (!node || typeof node !== "object" || Array.isArray(node)) {
|
||||
@@ -689,18 +710,29 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
updatedAt: normalizedUpdatedAt,
|
||||
})
|
||||
) {
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.reusedNodeCount += 1;
|
||||
}
|
||||
return baseNode;
|
||||
}
|
||||
const plainNode = clonePersistSnapshotRecord(node);
|
||||
if (!plainNode || typeof plainNode !== "object" || Array.isArray(plainNode)) {
|
||||
return null;
|
||||
}
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.clonedNodeCount += 1;
|
||||
}
|
||||
plainNode.id = id;
|
||||
plainNode.updatedAt = normalizedUpdatedAt;
|
||||
return plainNode;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.nodeCount = nodes.length;
|
||||
snapshotDiagnostics.nodesMs = readPersistDeltaNow() - nodesStartedAt;
|
||||
}
|
||||
|
||||
const edgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const edges = toArray(runtimeGraph?.edges)
|
||||
.map((edge) => {
|
||||
if (!edge || typeof edge !== "object" || Array.isArray(edge)) {
|
||||
@@ -719,12 +751,18 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
updatedAt: normalizedUpdatedAt,
|
||||
})
|
||||
) {
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.reusedEdgeCount += 1;
|
||||
}
|
||||
return baseEdge;
|
||||
}
|
||||
const plainEdge = clonePersistSnapshotRecord(edge);
|
||||
if (!plainEdge || typeof plainEdge !== "object" || Array.isArray(plainEdge)) {
|
||||
return null;
|
||||
}
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.clonedEdgeCount += 1;
|
||||
}
|
||||
plainEdge.id = id;
|
||||
plainEdge.fromId = normalizedFromId;
|
||||
plainEdge.toId = normalizedToId;
|
||||
@@ -732,7 +770,12 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
return plainEdge;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.edgeCount = edges.length;
|
||||
snapshotDiagnostics.edgesMs = readPersistDeltaNow() - edgesStartedAt;
|
||||
}
|
||||
|
||||
const tombstonesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const tombstones = toArray(options.tombstones ?? baseSnapshotView.tombstones)
|
||||
.map((record) => {
|
||||
if (!record || typeof record !== "object" || Array.isArray(record))
|
||||
@@ -752,12 +795,18 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
deletedAt: normalizedDeletedAt,
|
||||
})
|
||||
) {
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.reusedTombstoneCount += 1;
|
||||
}
|
||||
return baseTombstone;
|
||||
}
|
||||
const plainRecord = clonePersistSnapshotRecord(record);
|
||||
if (!plainRecord || typeof plainRecord !== "object" || Array.isArray(plainRecord)) {
|
||||
return null;
|
||||
}
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.clonedTombstoneCount += 1;
|
||||
}
|
||||
plainRecord.id = id;
|
||||
plainRecord.kind = normalizedKind;
|
||||
plainRecord.targetId = normalizedTargetId;
|
||||
@@ -766,7 +815,13 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
return plainRecord;
|
||||
})
|
||||
.filter(Boolean);
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.tombstoneCount = tombstones.length;
|
||||
snapshotDiagnostics.tombstonesMs =
|
||||
readPersistDeltaNow() - tombstonesStartedAt;
|
||||
}
|
||||
|
||||
const stateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const state = {
|
||||
...normalizeStateSnapshot(baseSnapshot),
|
||||
...(options.state || {}),
|
||||
@@ -783,7 +838,11 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
? Number(runtimeGraph.historyState.extractionCount)
|
||||
: META_DEFAULT_EXTRACTION_COUNT,
|
||||
};
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.stateMs = readPersistDeltaNow() - stateStartedAt;
|
||||
}
|
||||
|
||||
const metaStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const mergedMeta = {
|
||||
...baseSnapshot.meta,
|
||||
...(options.meta || {}),
|
||||
@@ -869,14 +928,26 @@ export function buildSnapshotFromGraph(graph, options = {}) {
|
||||
? Number(runtimeGraph.version)
|
||||
: Number(baseSnapshot.meta?.[BME_RUNTIME_GRAPH_VERSION_META_KEY] || 0),
|
||||
};
|
||||
if (snapshotDiagnostics) {
|
||||
snapshotDiagnostics.metaMs = readPersistDeltaNow() - metaStartedAt;
|
||||
}
|
||||
|
||||
return {
|
||||
const snapshotResult = {
|
||||
meta: mergedMeta,
|
||||
nodes,
|
||||
edges,
|
||||
tombstones,
|
||||
state,
|
||||
};
|
||||
if (snapshotDiagnostics) {
|
||||
emitOptionalDiagnostics(options, {
|
||||
...snapshotDiagnostics,
|
||||
runtimeMetaKeyCount: Object.keys(mergedMeta).length,
|
||||
totalMs: readPersistDeltaNow() - snapshotStartedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return snapshotResult;
|
||||
}
|
||||
|
||||
function normalizeSnapshotMetaState(snapshot = {}) {
|
||||
@@ -1630,7 +1701,7 @@ function readPersistDeltaNow() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
|
||||
function emitOptionalDiagnostics(options = {}, snapshot = null) {
|
||||
if (typeof options?.onDiagnostics !== "function") return;
|
||||
try {
|
||||
options.onDiagnostics(snapshot ? toPlainData(snapshot, snapshot) : null);
|
||||
@@ -1639,6 +1710,10 @@ function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
|
||||
}
|
||||
}
|
||||
|
||||
function emitPersistDeltaDiagnostics(options = {}, snapshot = null) {
|
||||
emitOptionalDiagnostics(options, snapshot);
|
||||
}
|
||||
|
||||
function tryBuildNativePersistDelta(
|
||||
beforeSnapshot,
|
||||
afterSnapshot,
|
||||
@@ -2016,6 +2091,23 @@ export function buildPersistDelta(beforeSnapshot, afterSnapshot, options = {}) {
|
||||
}
|
||||
|
||||
export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
const shouldCollectDiagnostics = typeof options?.onDiagnostics === "function";
|
||||
const hydrateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const hydrateDiagnostics = shouldCollectDiagnostics
|
||||
? {
|
||||
success: false,
|
||||
nodeCount: 0,
|
||||
edgeCount: 0,
|
||||
tombstoneCount: 0,
|
||||
nodesMs: 0,
|
||||
edgesMs: 0,
|
||||
runtimeMetaMs: 0,
|
||||
stateMs: 0,
|
||||
normalizeMs: 0,
|
||||
integrityMs: 0,
|
||||
integrityReasonCount: 0,
|
||||
}
|
||||
: null;
|
||||
const snapshotView = normalizePersistSnapshotView(snapshot);
|
||||
const snapshotMeta =
|
||||
snapshotView.meta &&
|
||||
@@ -2048,8 +2140,24 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
)
|
||||
? Number(snapshotMeta[BME_RUNTIME_GRAPH_VERSION_META_KEY])
|
||||
: runtimeGraph.version;
|
||||
|
||||
const hydrateNodesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
runtimeGraph.nodes = toArray(toPlainData(snapshotView.nodes, []));
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.nodeCount = runtimeGraph.nodes.length;
|
||||
hydrateDiagnostics.nodesMs = readPersistDeltaNow() - hydrateNodesStartedAt;
|
||||
}
|
||||
|
||||
const hydrateEdgesStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
runtimeGraph.edges = toArray(toPlainData(snapshotView.edges, []));
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.edgeCount = runtimeGraph.edges.length;
|
||||
hydrateDiagnostics.edgesMs = readPersistDeltaNow() - hydrateEdgesStartedAt;
|
||||
}
|
||||
|
||||
const hydrateRuntimeMetaStartedAt = shouldCollectDiagnostics
|
||||
? readPersistDeltaNow()
|
||||
: 0;
|
||||
runtimeGraph.batchJournal = toArray(
|
||||
toPlainData(snapshotMeta?.[BME_RUNTIME_BATCH_JOURNAL_META_KEY], []),
|
||||
);
|
||||
@@ -2076,6 +2184,10 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
snapshotMeta?.[BME_RUNTIME_SUMMARY_STATE_META_KEY],
|
||||
runtimeGraph.summaryState || {},
|
||||
);
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.runtimeMetaMs =
|
||||
readPersistDeltaNow() - hydrateRuntimeMetaStartedAt;
|
||||
}
|
||||
const rawKnowledgeState =
|
||||
runtimeGraph.knowledgeState &&
|
||||
typeof runtimeGraph.knowledgeState === "object" &&
|
||||
@@ -2095,6 +2207,7 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
? runtimeGraph.timelineState
|
||||
: {};
|
||||
|
||||
const hydrateStateStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
runtimeGraph.historyState = {
|
||||
...(runtimeGraph.historyState || {}),
|
||||
...snapshotHistoryState,
|
||||
@@ -2183,8 +2296,16 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
)
|
||||
? Number(snapshotMeta[BME_RUNTIME_LAST_PROCESSED_SEQ_META_KEY])
|
||||
: Number(runtimeGraph.historyState.lastProcessedAssistantFloor);
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.tombstoneCount = toArray(snapshotView.tombstones).length;
|
||||
hydrateDiagnostics.stateMs = readPersistDeltaNow() - hydrateStateStartedAt;
|
||||
}
|
||||
|
||||
const normalizeStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
const normalizedGraph = normalizeGraphRuntimeState(runtimeGraph, chatId);
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.normalizeMs = readPersistDeltaNow() - normalizeStartedAt;
|
||||
}
|
||||
if (
|
||||
normalizedGraph.knowledgeState &&
|
||||
typeof normalizedGraph.knowledgeState === "object" &&
|
||||
@@ -2238,6 +2359,7 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
);
|
||||
const inconsistentReasons = [];
|
||||
|
||||
const integrityStartedAt = shouldCollectDiagnostics ? readPersistDeltaNow() : 0;
|
||||
if (
|
||||
Number.isFinite(resolvedLastProcessedFloor) &&
|
||||
Number.isFinite(resolvedLastProcessedSeq) &&
|
||||
@@ -2255,8 +2377,20 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
if (collectionId && collectionId !== expectedCollectionId) {
|
||||
inconsistentReasons.push("vector-collection-mismatch");
|
||||
}
|
||||
if (hydrateDiagnostics) {
|
||||
hydrateDiagnostics.integrityMs = readPersistDeltaNow() - integrityStartedAt;
|
||||
hydrateDiagnostics.integrityReasonCount = inconsistentReasons.length;
|
||||
}
|
||||
|
||||
if (inconsistentReasons.length > 0) {
|
||||
if (hydrateDiagnostics) {
|
||||
emitOptionalDiagnostics(options, {
|
||||
...hydrateDiagnostics,
|
||||
success: false,
|
||||
integrityReasons: [...inconsistentReasons],
|
||||
totalMs: readPersistDeltaNow() - hydrateStartedAt,
|
||||
});
|
||||
}
|
||||
const error = new Error(
|
||||
`图谱快照完整性校验失败: ${inconsistentReasons.join(", ")}`,
|
||||
);
|
||||
@@ -2266,6 +2400,15 @@ export function buildGraphFromSnapshot(snapshot, options = {}) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (hydrateDiagnostics) {
|
||||
emitOptionalDiagnostics(options, {
|
||||
...hydrateDiagnostics,
|
||||
success: true,
|
||||
integrityReasons: [],
|
||||
totalMs: readPersistDeltaNow() - hydrateStartedAt,
|
||||
});
|
||||
}
|
||||
|
||||
return normalizedGraph;
|
||||
}
|
||||
|
||||
@@ -3151,6 +3294,40 @@ export class BmeDatabase {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async exportSnapshotProbe() {
|
||||
const db = await this.open();
|
||||
const metaRows = await db.transaction("r", db.table("meta"), async () =>
|
||||
await db.table("meta").toArray(),
|
||||
);
|
||||
const metaMap = toMetaMap(metaRows);
|
||||
const meta = {
|
||||
...metaMap,
|
||||
schemaVersion: BME_DB_SCHEMA_VERSION,
|
||||
chatId: this.chatId,
|
||||
revision: normalizeRevision(metaMap?.revision),
|
||||
nodeCount: normalizeNonNegativeInteger(metaMap?.nodeCount, 0),
|
||||
edgeCount: normalizeNonNegativeInteger(metaMap?.edgeCount, 0),
|
||||
tombstoneCount: normalizeNonNegativeInteger(metaMap?.tombstoneCount, 0),
|
||||
};
|
||||
const state = {
|
||||
lastProcessedFloor: Number.isFinite(Number(meta.lastProcessedFloor))
|
||||
? Number(meta.lastProcessedFloor)
|
||||
: META_DEFAULT_LAST_PROCESSED_FLOOR,
|
||||
extractionCount: Number.isFinite(Number(meta.extractionCount))
|
||||
? Number(meta.extractionCount)
|
||||
: META_DEFAULT_EXTRACTION_COUNT,
|
||||
};
|
||||
return {
|
||||
meta,
|
||||
state,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
__stBmeProbeOnly: true,
|
||||
__stBmeTombstonesOmitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async importSnapshot(snapshot, options = {}) {
|
||||
const db = await this.open();
|
||||
const normalizedSnapshot = sanitizeSnapshot(snapshot);
|
||||
|
||||
@@ -426,12 +426,16 @@ async function readJsonFile(parentHandle, name, fallbackValue = null) {
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
async function writeJsonFile(parentHandle, name, value) {
|
||||
async function writeJsonFile(parentHandle, name, value, options = {}) {
|
||||
const serializedText =
|
||||
typeof options?.serializedText === "string"
|
||||
? options.serializedText
|
||||
: JSON.stringify(value);
|
||||
const fileHandle = await parentHandle.getFileHandle(String(name || ""), {
|
||||
create: true,
|
||||
});
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(JSON.stringify(value));
|
||||
await writable.write(serializedText);
|
||||
await writable.close();
|
||||
return fileHandle;
|
||||
}
|
||||
@@ -2416,12 +2420,18 @@ export class OpfsGraphStore {
|
||||
runtimeMetaPatch: normalizedDelta.runtimeMetaPatch,
|
||||
countDelta: nextCountDelta,
|
||||
};
|
||||
const walSerializeStartedAt = readPersistCommitNow();
|
||||
const walSerializedText = JSON.stringify(walRecord);
|
||||
const walSerializeMs = readPersistCommitNow() - walSerializeStartedAt;
|
||||
const walWriteStartedAt = readPersistCommitNow();
|
||||
const walDirectory = await this._getWalDirectory();
|
||||
const walFilename = buildOpfsV2WalFilename(nextRevision);
|
||||
await writeJsonFile(walDirectory, walFilename, walRecord);
|
||||
const walByteLength = JSON.stringify(walRecord).length;
|
||||
const walWriteMs = readPersistCommitNow() - walWriteStartedAt;
|
||||
await writeJsonFile(walDirectory, walFilename, walRecord, {
|
||||
serializedText: walSerializedText,
|
||||
});
|
||||
const walByteLength = walSerializedText.length;
|
||||
const walFileWriteMs = readPersistCommitNow() - walWriteStartedAt;
|
||||
const walWriteMs = walSerializeMs + walFileWriteMs;
|
||||
|
||||
const hadPendingWal =
|
||||
normalizeRevision(manifest?.pendingLogFromRevision) <= currentHeadRevision;
|
||||
@@ -2448,12 +2458,40 @@ export class OpfsGraphStore {
|
||||
lastReason: reason,
|
||||
},
|
||||
};
|
||||
const manifestWriteStartedAt = readPersistCommitNow();
|
||||
await this._writeManifest(nextManifest);
|
||||
const manifestWriteMs = readPersistCommitNow() - manifestWriteStartedAt;
|
||||
const manifestWriteDiagnostics = {};
|
||||
await this._writeManifest(nextManifest, {
|
||||
diagnostics: manifestWriteDiagnostics,
|
||||
});
|
||||
const manifestSerializeMs = Number(
|
||||
manifestWriteDiagnostics.serializeMs || 0,
|
||||
);
|
||||
const manifestFileWriteMs = Number(
|
||||
manifestWriteDiagnostics.writeMs || 0,
|
||||
);
|
||||
const manifestWriteMs = manifestSerializeMs + manifestFileWriteMs;
|
||||
|
||||
const committedSnapshot =
|
||||
options?.committedSnapshot &&
|
||||
typeof options.committedSnapshot === "object" &&
|
||||
!Array.isArray(options.committedSnapshot)
|
||||
? sanitizeSnapshot(options.committedSnapshot)
|
||||
: null;
|
||||
let cacheApplyMs = 0;
|
||||
if (this._snapshotCache) {
|
||||
if (committedSnapshot) {
|
||||
const cacheApplyStartedAt = readPersistCommitNow();
|
||||
committedSnapshot.meta = {
|
||||
...committedSnapshot.meta,
|
||||
...nextMeta,
|
||||
};
|
||||
committedSnapshot.state = normalizeSnapshotState(committedSnapshot);
|
||||
committedSnapshot.meta.lastProcessedFloor = committedSnapshot.state.lastProcessedFloor;
|
||||
committedSnapshot.meta.extractionCount = committedSnapshot.state.extractionCount;
|
||||
committedSnapshot.meta.nodeCount = committedSnapshot.nodes.length;
|
||||
committedSnapshot.meta.edgeCount = committedSnapshot.edges.length;
|
||||
committedSnapshot.meta.tombstoneCount = committedSnapshot.tombstones.length;
|
||||
this._snapshotCache = committedSnapshot;
|
||||
cacheApplyMs = readPersistCommitNow() - cacheApplyStartedAt;
|
||||
} else if (this._snapshotCache) {
|
||||
const cacheApplyStartedAt = readPersistCommitNow();
|
||||
const nextSnapshot = applyOpfsV2DeltaToSnapshot(
|
||||
this._snapshotCache,
|
||||
@@ -2494,7 +2532,11 @@ export class OpfsGraphStore {
|
||||
readPersistCommitNow() - commitStartedAt,
|
||||
),
|
||||
manifestReadMs: normalizePersistCommitMs(manifestReadMs),
|
||||
walSerializeMs: normalizePersistCommitMs(walSerializeMs),
|
||||
walFileWriteMs: normalizePersistCommitMs(walFileWriteMs),
|
||||
walWriteMs: normalizePersistCommitMs(walWriteMs),
|
||||
manifestSerializeMs: normalizePersistCommitMs(manifestSerializeMs),
|
||||
manifestFileWriteMs: normalizePersistCommitMs(manifestFileWriteMs),
|
||||
manifestWriteMs: normalizePersistCommitMs(manifestWriteMs),
|
||||
cacheApplyMs: normalizePersistCommitMs(cacheApplyMs),
|
||||
payloadBytes,
|
||||
@@ -2740,6 +2782,39 @@ export class OpfsGraphStore {
|
||||
return exported;
|
||||
}
|
||||
|
||||
async exportSnapshotProbe() {
|
||||
const manifest = await this._ensureV2Ready();
|
||||
const meta = {
|
||||
...createDefaultMetaValues(this.chatId),
|
||||
...(manifest?.meta || {}),
|
||||
chatId: this.chatId,
|
||||
revision: normalizeRevision(manifest?.headRevision || manifest?.meta?.revision),
|
||||
nodeCount: normalizeNonNegativeInteger(manifest?.meta?.nodeCount, 0),
|
||||
edgeCount: normalizeNonNegativeInteger(manifest?.meta?.edgeCount, 0),
|
||||
tombstoneCount: normalizeNonNegativeInteger(manifest?.meta?.tombstoneCount, 0),
|
||||
storagePrimary: OPFS_STORE_KIND,
|
||||
storageMode: this.storeMode,
|
||||
schemaVersion: BME_DB_SCHEMA_VERSION,
|
||||
};
|
||||
const state = {
|
||||
lastProcessedFloor: Number.isFinite(Number(meta.lastProcessedFloor))
|
||||
? Number(meta.lastProcessedFloor)
|
||||
: META_DEFAULT_LAST_PROCESSED_FLOOR,
|
||||
extractionCount: Number.isFinite(Number(meta.extractionCount))
|
||||
? Number(meta.extractionCount)
|
||||
: META_DEFAULT_EXTRACTION_COUNT,
|
||||
};
|
||||
return {
|
||||
meta,
|
||||
state,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
tombstones: [],
|
||||
__stBmeProbeOnly: true,
|
||||
__stBmeTombstonesOmitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async importSnapshot(snapshot, options = {}) {
|
||||
return await this._runSerializedWrite("importSnapshot", async () => {
|
||||
await this._ensureV2Ready({ awaitWrites: false });
|
||||
@@ -2993,7 +3068,7 @@ export class OpfsGraphStore {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
async _writeManifest(manifest = {}) {
|
||||
async _writeManifest(manifest = {}, options = {}) {
|
||||
const chatDirectory = await this._getChatDirectory();
|
||||
const nextManifest = {
|
||||
...manifest,
|
||||
@@ -3017,7 +3092,24 @@ export class OpfsGraphStore {
|
||||
storageMode: this.storeMode,
|
||||
},
|
||||
};
|
||||
await writeJsonFile(chatDirectory, OPFS_MANIFEST_FILENAME, nextManifest);
|
||||
let serializedText = "";
|
||||
let serializeMs = 0;
|
||||
if (options?.diagnostics && typeof options.diagnostics === "object") {
|
||||
const serializeStartedAt = readPersistCommitNow();
|
||||
serializedText = JSON.stringify(nextManifest);
|
||||
serializeMs = readPersistCommitNow() - serializeStartedAt;
|
||||
}
|
||||
const writeStartedAt =
|
||||
options?.diagnostics && typeof options.diagnostics === "object"
|
||||
? readPersistCommitNow()
|
||||
: 0;
|
||||
await writeJsonFile(chatDirectory, OPFS_MANIFEST_FILENAME, nextManifest, {
|
||||
serializedText,
|
||||
});
|
||||
if (options?.diagnostics && typeof options.diagnostics === "object") {
|
||||
options.diagnostics.serializeMs = serializeMs;
|
||||
options.diagnostics.writeMs = readPersistCommitNow() - writeStartedAt;
|
||||
}
|
||||
this._manifestCache = nextManifest;
|
||||
return nextManifest;
|
||||
}
|
||||
|
||||
327
sync/bme-sync.js
327
sync/bme-sync.js
@@ -47,6 +47,30 @@ export function buildRestoreSafetyChatId(chatId) {
|
||||
return `__restore_safety__${normalizeChatId(chatId)}`;
|
||||
}
|
||||
|
||||
function readSyncTimingNow() {
|
||||
if (typeof performance === "object" && typeof performance.now === "function") {
|
||||
return performance.now();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function normalizeSyncTimingMs(value = 0) {
|
||||
return Math.round((Number(value) || 0) * 10) / 10;
|
||||
}
|
||||
|
||||
function finalizeSyncTimings(record = {}, startedAt = 0) {
|
||||
const result = {};
|
||||
for (const [key, value] of Object.entries(record || {})) {
|
||||
if (typeof value === "number" && Number.isFinite(value)) {
|
||||
result[key] = normalizeSyncTimingMs(value);
|
||||
}
|
||||
}
|
||||
if (startedAt > 0) {
|
||||
result.totalMs = normalizeSyncTimingMs(readSyncTimingNow() - startedAt);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function resolveCloudStorageMode(options = {}) {
|
||||
const mode =
|
||||
typeof options.getCloudStorageMode === "function"
|
||||
@@ -494,14 +518,20 @@ async function resolveBackupLookupContext(chatId, options = {}) {
|
||||
}
|
||||
|
||||
async function readBackupEnvelope(chatId, options = {}) {
|
||||
const readStartedAt = readSyncTimingNow();
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const lookupStartedAt = readSyncTimingNow();
|
||||
const lookup = await resolveBackupLookupContext(normalizedChatId, options);
|
||||
const lookupMs = readSyncTimingNow() - lookupStartedAt;
|
||||
const fetchImpl = getFetch(options);
|
||||
const fallbackFilename = buildBackupFilename(normalizedChatId);
|
||||
let lastMissingFilename = lookup.candidates[0]?.filename || fallbackFilename;
|
||||
let networkMs = 0;
|
||||
let parseMs = 0;
|
||||
|
||||
for (const candidate of lookup.candidates) {
|
||||
try {
|
||||
const networkStartedAt = readSyncTimingNow();
|
||||
const response = await fetchImpl(
|
||||
`${candidate.serverPath || `/user/files/${encodeURIComponent(candidate.filename)}`}?t=${Date.now()}`,
|
||||
{
|
||||
@@ -509,6 +539,7 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
cache: "no-store",
|
||||
},
|
||||
);
|
||||
networkMs += readSyncTimingNow() - networkStartedAt;
|
||||
if (response.status === 404) {
|
||||
lastMissingFilename = candidate.filename;
|
||||
continue;
|
||||
@@ -521,10 +552,13 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
envelope: null,
|
||||
reason: "backup-read-error",
|
||||
error: new Error(errorText || `HTTP ${response.status}`),
|
||||
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
|
||||
};
|
||||
}
|
||||
|
||||
const parseStartedAt = readSyncTimingNow();
|
||||
const payload = await response.json();
|
||||
parseMs += readSyncTimingNow() - parseStartedAt;
|
||||
const envelope = normalizeBackupEnvelope(payload, normalizedChatId);
|
||||
if (!envelope) {
|
||||
return {
|
||||
@@ -532,6 +566,7 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
filename: candidate.filename,
|
||||
envelope: null,
|
||||
reason: "invalid-backup",
|
||||
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -539,6 +574,7 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
filename: candidate.filename,
|
||||
envelope,
|
||||
reason: "ok",
|
||||
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
@@ -547,6 +583,7 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
envelope: null,
|
||||
reason: "backup-read-error",
|
||||
error,
|
||||
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -557,6 +594,7 @@ async function readBackupEnvelope(chatId, options = {}) {
|
||||
envelope: null,
|
||||
reason: "not-found",
|
||||
manifestError: lookup.manifestError,
|
||||
timings: finalizeSyncTimings({ lookupMs, networkMs, parseMs }, readStartedAt),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -581,10 +619,14 @@ async function syncDeletedBackupMeta(chatId, remainingEntry, options = {}) {
|
||||
}
|
||||
|
||||
async function writeBackupEnvelope(envelope, chatId, options = {}) {
|
||||
const writeStartedAt = readSyncTimingNow();
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const filename = buildBackupFilename(normalizedChatId);
|
||||
const fetchImpl = getFetch(options);
|
||||
const serializeStartedAt = readSyncTimingNow();
|
||||
const payload = JSON.stringify(envelope);
|
||||
const serializeMs = readSyncTimingNow() - serializeStartedAt;
|
||||
const uploadStartedAt = readSyncTimingNow();
|
||||
const response = await fetchImpl("/api/files/upload", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -596,16 +638,27 @@ async function writeBackupEnvelope(envelope, chatId, options = {}) {
|
||||
data: encodeBase64Utf8(payload),
|
||||
}),
|
||||
});
|
||||
const uploadMs = readSyncTimingNow() - uploadStartedAt;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(errorText || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const responseParseStartedAt = readSyncTimingNow();
|
||||
const uploadResult = await response.json().catch(() => ({}));
|
||||
const responseParseMs = readSyncTimingNow() - responseParseStartedAt;
|
||||
return {
|
||||
filename,
|
||||
path: String(uploadResult?.path || `/user/files/${filename}`),
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
serializeMs,
|
||||
uploadMs,
|
||||
responseParseMs,
|
||||
},
|
||||
writeStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1825,6 +1878,7 @@ async function resolveSyncFilenameCandidates(chatId, options = {}) {
|
||||
}
|
||||
|
||||
async function readRemoteSnapshot(chatId, options = {}) {
|
||||
const readStartedAt = readSyncTimingNow();
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
if (!normalizedChatId) {
|
||||
return {
|
||||
@@ -1832,15 +1886,22 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
status: "missing-chat-id",
|
||||
filename: "",
|
||||
snapshot: null,
|
||||
timings: finalizeSyncTimings({}, readStartedAt),
|
||||
};
|
||||
}
|
||||
|
||||
const fetchImpl = getFetch(options);
|
||||
const resolveStartedAt = readSyncTimingNow();
|
||||
const candidateFilenames = await resolveSyncFilenameCandidates(
|
||||
normalizedChatId,
|
||||
options,
|
||||
);
|
||||
const resolveCandidatesMs = readSyncTimingNow() - resolveStartedAt;
|
||||
let lastNotFoundFilename = candidateFilenames[0] || "";
|
||||
let networkMs = 0;
|
||||
let parseMs = 0;
|
||||
let chunkReadMs = 0;
|
||||
let normalizeMs = 0;
|
||||
|
||||
for (const filename of candidateFilenames) {
|
||||
const cacheBust = `t=${Date.now()}`;
|
||||
@@ -1848,10 +1909,12 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
|
||||
let response;
|
||||
try {
|
||||
const networkStartedAt = readSyncTimingNow();
|
||||
response = await fetchImpl(url, {
|
||||
method: "GET",
|
||||
cache: "no-store",
|
||||
});
|
||||
networkMs += readSyncTimingNow() - networkStartedAt;
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 读取远端同步文件失败:", error);
|
||||
return {
|
||||
@@ -1860,6 +1923,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
timings: finalizeSyncTimings(
|
||||
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1879,14 +1946,20 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
snapshot: null,
|
||||
error,
|
||||
statusCode: response.status,
|
||||
timings: finalizeSyncTimings(
|
||||
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parseStartedAt = readSyncTimingNow();
|
||||
const remotePayload = await response.json();
|
||||
parseMs += readSyncTimingNow() - parseStartedAt;
|
||||
let snapshot = null;
|
||||
if (Number(remotePayload?.formatVersion || 0) === BME_REMOTE_SYNC_FORMAT_VERSION_V2) {
|
||||
snapshot = await readRemoteSnapshotV2Manifest(
|
||||
const manifestResult = await readRemoteSnapshotV2Manifest(
|
||||
remotePayload,
|
||||
normalizedChatId,
|
||||
{
|
||||
@@ -1894,8 +1967,13 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
filename,
|
||||
},
|
||||
);
|
||||
snapshot = manifestResult.snapshot;
|
||||
chunkReadMs += Number(manifestResult?.timings?.chunkReadMs || 0);
|
||||
normalizeMs += Number(manifestResult?.timings?.normalizeMs || 0);
|
||||
} else {
|
||||
const normalizeStartedAt = readSyncTimingNow();
|
||||
snapshot = normalizeSyncSnapshot(remotePayload, normalizedChatId);
|
||||
normalizeMs += readSyncTimingNow() - normalizeStartedAt;
|
||||
}
|
||||
rememberResolvedSyncFilename(normalizedChatId, filename);
|
||||
return {
|
||||
@@ -1903,6 +1981,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
status: "ok",
|
||||
filename,
|
||||
snapshot,
|
||||
timings: finalizeSyncTimings(
|
||||
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 解析远端同步文件失败:", error);
|
||||
@@ -1912,6 +1994,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
filename,
|
||||
snapshot: null,
|
||||
error,
|
||||
timings: finalizeSyncTimings(
|
||||
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1921,6 +2007,10 @@ async function readRemoteSnapshot(chatId, options = {}) {
|
||||
status: "not-found",
|
||||
filename: lastNotFoundFilename,
|
||||
snapshot: null,
|
||||
timings: finalizeSyncTimings(
|
||||
{ resolveCandidatesMs, networkMs, parseMs, chunkReadMs, normalizeMs },
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1944,17 +2034,21 @@ async function readRemoteJsonFile(filename, options = {}) {
|
||||
}
|
||||
|
||||
async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options = {}) {
|
||||
const readStartedAt = readSyncTimingNow();
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const chunks = Array.isArray(manifest?.chunks) ? manifest.chunks : [];
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
const tombstones = [];
|
||||
let runtimeMeta = {};
|
||||
let chunkReadMs = 0;
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const filename = String(chunk?.filename || "").trim();
|
||||
if (!filename) continue;
|
||||
const chunkStartedAt = readSyncTimingNow();
|
||||
const payload = await readRemoteJsonFile(filename, options);
|
||||
chunkReadMs += readSyncTimingNow() - chunkStartedAt;
|
||||
const records = Array.isArray(payload?.records) ? payload.records : [];
|
||||
switch (String(chunk.kind || "").trim()) {
|
||||
case "nodes":
|
||||
@@ -1977,7 +2071,8 @@ async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeSyncSnapshot(
|
||||
const normalizeStartedAt = readSyncTimingNow();
|
||||
const snapshot = normalizeSyncSnapshot(
|
||||
{
|
||||
meta: {
|
||||
...runtimeMeta,
|
||||
@@ -1994,55 +2089,94 @@ async function readRemoteSnapshotV2Manifest(manifest = {}, chatId = "", options
|
||||
},
|
||||
normalizedChatId,
|
||||
);
|
||||
const normalizeMs = readSyncTimingNow() - normalizeStartedAt;
|
||||
return {
|
||||
snapshot,
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
chunkReadMs,
|
||||
normalizeMs,
|
||||
},
|
||||
readStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function writeSnapshotToRemote(snapshot, chatId, options = {}) {
|
||||
const writeStartedAt = readSyncTimingNow();
|
||||
const normalizedChatId = normalizeChatId(chatId);
|
||||
const normalizedSnapshot = normalizeSyncSnapshot(snapshot, normalizedChatId);
|
||||
const filename = await resolveSyncFilename(normalizedChatId, options);
|
||||
const fetchImpl = getFetch(options);
|
||||
const envelopeBuildStartedAt = readSyncTimingNow();
|
||||
const syncEnvelope = buildRemoteSyncEnvelopeV2(
|
||||
normalizedSnapshot,
|
||||
normalizedChatId,
|
||||
filename,
|
||||
);
|
||||
const envelopeBuildMs = readSyncTimingNow() - envelopeBuildStartedAt;
|
||||
const requestHeaders = {
|
||||
...getRequestHeadersSafe(options),
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
let chunkSerializeMs = 0;
|
||||
let chunkUploadMs = 0;
|
||||
for (const chunk of syncEnvelope.chunks) {
|
||||
const serializeStartedAt = readSyncTimingNow();
|
||||
const chunkPayload = JSON.stringify(chunk.payload, null, 2);
|
||||
chunkSerializeMs += readSyncTimingNow() - serializeStartedAt;
|
||||
const uploadStartedAt = readSyncTimingNow();
|
||||
const chunkResponse = await fetchImpl("/api/files/upload", {
|
||||
method: "POST",
|
||||
headers: requestHeaders,
|
||||
body: JSON.stringify({
|
||||
name: chunk.filename,
|
||||
data: encodeBase64Utf8(JSON.stringify(chunk.payload, null, 2)),
|
||||
data: encodeBase64Utf8(chunkPayload),
|
||||
}),
|
||||
});
|
||||
chunkUploadMs += readSyncTimingNow() - uploadStartedAt;
|
||||
if (!chunkResponse.ok) {
|
||||
const errorText = await chunkResponse.text().catch(() => chunkResponse.statusText);
|
||||
throw new Error(errorText || `HTTP ${chunkResponse.status}`);
|
||||
}
|
||||
}
|
||||
const manifestSerializeStartedAt = readSyncTimingNow();
|
||||
const manifestPayload = JSON.stringify(syncEnvelope.manifest, null, 2);
|
||||
const manifestSerializeMs = readSyncTimingNow() - manifestSerializeStartedAt;
|
||||
const manifestUploadStartedAt = readSyncTimingNow();
|
||||
const response = await fetchImpl("/api/files/upload", {
|
||||
method: "POST",
|
||||
headers: requestHeaders,
|
||||
body: JSON.stringify({
|
||||
name: filename,
|
||||
data: encodeBase64Utf8(JSON.stringify(syncEnvelope.manifest, null, 2)),
|
||||
data: encodeBase64Utf8(manifestPayload),
|
||||
}),
|
||||
});
|
||||
const manifestUploadMs = readSyncTimingNow() - manifestUploadStartedAt;
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => response.statusText);
|
||||
throw new Error(errorText || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const responseParseStartedAt = readSyncTimingNow();
|
||||
const uploadResult = await response.json().catch(() => ({}));
|
||||
const responseParseMs = readSyncTimingNow() - responseParseStartedAt;
|
||||
return {
|
||||
filename,
|
||||
path: String(uploadResult?.path || ""),
|
||||
payload: syncEnvelope.manifest,
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
envelopeBuildMs,
|
||||
chunkSerializeMs,
|
||||
chunkUploadMs,
|
||||
manifestSerializeMs,
|
||||
manifestUploadMs,
|
||||
responseParseMs,
|
||||
},
|
||||
writeStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2160,15 +2294,19 @@ export async function backupToServer(chatId, options = {}) {
|
||||
backedUp: false,
|
||||
chatId: "",
|
||||
reason: "missing-chat-id",
|
||||
timings: finalizeSyncTimings({}, readSyncTimingNow()),
|
||||
};
|
||||
}
|
||||
|
||||
const backupStartedAt = readSyncTimingNow();
|
||||
try {
|
||||
const db = await getDb(normalizedChatId, options);
|
||||
const exportStartedAt = readSyncTimingNow();
|
||||
const snapshot = normalizeSyncSnapshot(
|
||||
await db.exportSnapshot(),
|
||||
normalizedChatId,
|
||||
);
|
||||
const exportMs = readSyncTimingNow() - exportStartedAt;
|
||||
const nowMs = Date.now();
|
||||
const deviceId = getOrCreateDeviceId();
|
||||
|
||||
@@ -2179,6 +2317,7 @@ export async function backupToServer(chatId, options = {}) {
|
||||
nowMs,
|
||||
);
|
||||
|
||||
const envelopeBuildStartedAt = readSyncTimingNow();
|
||||
const backupSnapshot = buildManualBackupSnapshot(snapshot, normalizedChatId);
|
||||
const envelope = {
|
||||
kind: "st-bme-backup",
|
||||
@@ -2188,15 +2327,18 @@ export async function backupToServer(chatId, options = {}) {
|
||||
sourceDeviceId: deviceId,
|
||||
snapshot: backupSnapshot,
|
||||
};
|
||||
const envelopeBuildMs = readSyncTimingNow() - envelopeBuildStartedAt;
|
||||
|
||||
const uploadResult = await writeBackupEnvelope(
|
||||
envelope,
|
||||
normalizedChatId,
|
||||
options,
|
||||
);
|
||||
const uploadTimings = uploadResult?.timings || {};
|
||||
const serializedEnvelope = JSON.stringify(envelope);
|
||||
|
||||
try {
|
||||
const manifestWriteStartedAt = readSyncTimingNow();
|
||||
await upsertBackupManifestEntry(
|
||||
{
|
||||
filename: uploadResult.filename,
|
||||
@@ -2210,6 +2352,37 @@ export async function backupToServer(chatId, options = {}) {
|
||||
},
|
||||
options,
|
||||
);
|
||||
const manifestWriteMs = readSyncTimingNow() - manifestWriteStartedAt;
|
||||
const metaPatchStartedAt = readSyncTimingNow();
|
||||
await patchDbMeta(db, {
|
||||
deviceId,
|
||||
syncDirty: false,
|
||||
syncDirtyReason: "",
|
||||
lastBackupUploadedAt: nowMs,
|
||||
lastBackupFilename: uploadResult.filename,
|
||||
});
|
||||
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
|
||||
|
||||
return {
|
||||
backedUp: true,
|
||||
chatId: normalizedChatId,
|
||||
filename: uploadResult.filename,
|
||||
remotePath: uploadResult.path,
|
||||
revision: normalizeRevision(snapshot.meta.revision),
|
||||
backupTime: nowMs,
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
exportMs,
|
||||
envelopeBuildMs,
|
||||
uploadMs: Number(uploadTimings.totalMs || 0),
|
||||
envelopeSerializeMs: Number(uploadTimings.serializeMs || 0),
|
||||
envelopeResponseParseMs: Number(uploadTimings.responseParseMs || 0),
|
||||
manifestWriteMs,
|
||||
metaPatchMs,
|
||||
},
|
||||
backupStartedAt,
|
||||
),
|
||||
};
|
||||
} catch (manifestError) {
|
||||
return {
|
||||
backedUp: false,
|
||||
@@ -2219,25 +2392,18 @@ export async function backupToServer(chatId, options = {}) {
|
||||
reason: "backup-manifest-error",
|
||||
backupUploaded: true,
|
||||
error: manifestError,
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
exportMs,
|
||||
envelopeBuildMs,
|
||||
uploadMs: Number(uploadTimings.totalMs || 0),
|
||||
envelopeSerializeMs: Number(uploadTimings.serializeMs || 0),
|
||||
envelopeResponseParseMs: Number(uploadTimings.responseParseMs || 0),
|
||||
},
|
||||
backupStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
await patchDbMeta(db, {
|
||||
deviceId,
|
||||
syncDirty: false,
|
||||
syncDirtyReason: "",
|
||||
lastBackupUploadedAt: nowMs,
|
||||
lastBackupFilename: uploadResult.filename,
|
||||
});
|
||||
|
||||
return {
|
||||
backedUp: true,
|
||||
chatId: normalizedChatId,
|
||||
filename: uploadResult.filename,
|
||||
remotePath: uploadResult.path,
|
||||
revision: normalizeRevision(snapshot.meta.revision),
|
||||
backupTime: nowMs,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 手动备份到云端失败:", error);
|
||||
return {
|
||||
@@ -2245,6 +2411,7 @@ export async function backupToServer(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
reason: "backup-error",
|
||||
error,
|
||||
timings: finalizeSyncTimings({}, backupStartedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2256,18 +2423,30 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
restored: false,
|
||||
chatId: "",
|
||||
reason: "missing-chat-id",
|
||||
timings: finalizeSyncTimings({}, readSyncTimingNow()),
|
||||
};
|
||||
}
|
||||
|
||||
const restoreStartedAt = readSyncTimingNow();
|
||||
try {
|
||||
const db = await getDb(normalizedChatId, options);
|
||||
const remoteResult = await readBackupEnvelope(normalizedChatId, options);
|
||||
const downloadTimings = remoteResult?.timings || {};
|
||||
if (!remoteResult.exists || !remoteResult.envelope) {
|
||||
return {
|
||||
restored: false,
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename || "",
|
||||
reason: remoteResult.reason || "backup-missing",
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
downloadMs: Number(downloadTimings.totalMs || 0),
|
||||
lookupMs: Number(downloadTimings.lookupMs || 0),
|
||||
networkMs: Number(downloadTimings.networkMs || 0),
|
||||
envelopeParseMs: Number(downloadTimings.parseMs || 0),
|
||||
},
|
||||
restoreStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2278,6 +2457,15 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename,
|
||||
reason: "backup-version-mismatch",
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
downloadMs: Number(downloadTimings.totalMs || 0),
|
||||
lookupMs: Number(downloadTimings.lookupMs || 0),
|
||||
networkMs: Number(downloadTimings.networkMs || 0),
|
||||
envelopeParseMs: Number(downloadTimings.parseMs || 0),
|
||||
},
|
||||
restoreStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2287,6 +2475,15 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename,
|
||||
reason: "backup-chat-id-mismatch",
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
downloadMs: Number(downloadTimings.totalMs || 0),
|
||||
lookupMs: Number(downloadTimings.lookupMs || 0),
|
||||
networkMs: Number(downloadTimings.networkMs || 0),
|
||||
envelopeParseMs: Number(downloadTimings.parseMs || 0),
|
||||
},
|
||||
restoreStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2304,26 +2501,42 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename,
|
||||
reason: "snapshot-chat-id-mismatch",
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
downloadMs: Number(downloadTimings.totalMs || 0),
|
||||
lookupMs: Number(downloadTimings.lookupMs || 0),
|
||||
networkMs: Number(downloadTimings.networkMs || 0),
|
||||
envelopeParseMs: Number(downloadTimings.parseMs || 0),
|
||||
},
|
||||
restoreStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const localExportStartedAt = readSyncTimingNow();
|
||||
const localSnapshot = normalizeSyncSnapshot(
|
||||
await db.exportSnapshot(),
|
||||
normalizedChatId,
|
||||
);
|
||||
const localExportMs = readSyncTimingNow() - localExportStartedAt;
|
||||
const safetySnapshotStartedAt = readSyncTimingNow();
|
||||
await createRestoreSafetySnapshot(
|
||||
normalizedChatId,
|
||||
localSnapshot,
|
||||
options,
|
||||
);
|
||||
const safetySnapshotMs = readSyncTimingNow() - safetySnapshotStartedAt;
|
||||
|
||||
const importStartedAt = readSyncTimingNow();
|
||||
await db.importSnapshot(snapshot, {
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision: normalizeRevision(snapshot.meta.revision),
|
||||
markSyncDirty: false,
|
||||
});
|
||||
const importMs = readSyncTimingNow() - importStartedAt;
|
||||
|
||||
const metaPatchStartedAt = readSyncTimingNow();
|
||||
await patchDbMeta(db, {
|
||||
deviceId: getOrCreateDeviceId(),
|
||||
syncDirty: false,
|
||||
@@ -2332,12 +2545,15 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
lastBackupFilename:
|
||||
remoteResult.filename || buildBackupFilename(normalizedChatId),
|
||||
});
|
||||
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
|
||||
|
||||
const hookStartedAt = readSyncTimingNow();
|
||||
await invokeSyncAppliedHook(options, {
|
||||
chatId: normalizedChatId,
|
||||
action: "restore-backup",
|
||||
revision: normalizeRevision(snapshot.meta.revision),
|
||||
});
|
||||
const hookMs = readSyncTimingNow() - hookStartedAt;
|
||||
|
||||
return {
|
||||
restored: true,
|
||||
@@ -2345,6 +2561,20 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
filename: remoteResult.filename,
|
||||
revision: normalizeRevision(snapshot.meta.revision),
|
||||
backupTime: normalizeTimestamp(envelope.createdAt, 0),
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
downloadMs: Number(downloadTimings.totalMs || 0),
|
||||
lookupMs: Number(downloadTimings.lookupMs || 0),
|
||||
networkMs: Number(downloadTimings.networkMs || 0),
|
||||
envelopeParseMs: Number(downloadTimings.parseMs || 0),
|
||||
localExportMs,
|
||||
safetySnapshotMs,
|
||||
importMs,
|
||||
metaPatchMs,
|
||||
hookMs,
|
||||
},
|
||||
restoreStartedAt,
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 从云端恢复备份失败:", error);
|
||||
@@ -2353,6 +2583,7 @@ export async function restoreFromServer(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
reason: "restore-error",
|
||||
error,
|
||||
timings: finalizeSyncTimings({}, restoreStartedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2455,12 +2686,16 @@ export async function upload(chatId, options = {}) {
|
||||
uploaded: false,
|
||||
chatId: "",
|
||||
reason: "missing-chat-id",
|
||||
timings: finalizeSyncTimings({}, readSyncTimingNow()),
|
||||
};
|
||||
}
|
||||
|
||||
const uploadStartedAt = readSyncTimingNow();
|
||||
try {
|
||||
const db = await getDb(normalizedChatId, options);
|
||||
const exportStartedAt = readSyncTimingNow();
|
||||
const localSnapshot = normalizeSyncSnapshot(await db.exportSnapshot(), normalizedChatId);
|
||||
const exportMs = readSyncTimingNow() - exportStartedAt;
|
||||
const nowMs = Date.now();
|
||||
|
||||
const deviceId = getOrCreateDeviceId();
|
||||
@@ -2469,7 +2704,9 @@ export async function upload(chatId, options = {}) {
|
||||
localSnapshot.meta.lastModified = normalizeTimestamp(localSnapshot.meta.lastModified, nowMs);
|
||||
|
||||
const uploadResult = await writeSnapshotToRemote(localSnapshot, normalizedChatId, options);
|
||||
const uploadTimings = uploadResult?.timings || {};
|
||||
|
||||
const metaPatchStartedAt = readSyncTimingNow();
|
||||
await patchDbMeta(db, {
|
||||
deviceId,
|
||||
lastSyncUploadedAt: nowMs,
|
||||
@@ -2479,6 +2716,7 @@ export async function upload(chatId, options = {}) {
|
||||
lastModified: localSnapshot.meta.lastModified,
|
||||
remoteSyncFormatVersion: BME_REMOTE_SYNC_FORMAT_VERSION_V2,
|
||||
});
|
||||
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
|
||||
|
||||
return {
|
||||
uploaded: true,
|
||||
@@ -2486,6 +2724,19 @@ export async function upload(chatId, options = {}) {
|
||||
filename: uploadResult.filename,
|
||||
remotePath: uploadResult.path,
|
||||
revision: normalizeRevision(localSnapshot.meta.revision),
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
exportMs,
|
||||
envelopeBuildMs: Number(uploadTimings.envelopeBuildMs || 0),
|
||||
chunkSerializeMs: Number(uploadTimings.chunkSerializeMs || 0),
|
||||
chunkUploadMs: Number(uploadTimings.chunkUploadMs || 0),
|
||||
manifestSerializeMs: Number(uploadTimings.manifestSerializeMs || 0),
|
||||
manifestUploadMs: Number(uploadTimings.manifestUploadMs || 0),
|
||||
responseParseMs: Number(uploadTimings.responseParseMs || 0),
|
||||
metaPatchMs,
|
||||
},
|
||||
uploadStartedAt,
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 上传同步文件失败:", error);
|
||||
@@ -2494,6 +2745,7 @@ export async function upload(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
reason: "upload-error",
|
||||
error,
|
||||
timings: finalizeSyncTimings({}, uploadStartedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2506,12 +2758,15 @@ export async function download(chatId, options = {}) {
|
||||
exists: false,
|
||||
chatId: "",
|
||||
reason: "missing-chat-id",
|
||||
timings: finalizeSyncTimings({}, readSyncTimingNow()),
|
||||
};
|
||||
}
|
||||
|
||||
const downloadStartedAt = readSyncTimingNow();
|
||||
try {
|
||||
const db = await getDb(normalizedChatId, options);
|
||||
const remoteResult = await readRemoteSnapshot(normalizedChatId, options);
|
||||
const remoteTimings = remoteResult?.timings || {};
|
||||
|
||||
if (!remoteResult.exists || !remoteResult.snapshot) {
|
||||
return {
|
||||
@@ -2520,6 +2775,16 @@ export async function download(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename || "",
|
||||
reason: remoteResult.status || "remote-missing",
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
resolveCandidatesMs: Number(remoteTimings.resolveCandidatesMs || 0),
|
||||
networkMs: Number(remoteTimings.networkMs || 0),
|
||||
parseMs: Number(remoteTimings.parseMs || 0),
|
||||
chunkReadMs: Number(remoteTimings.chunkReadMs || 0),
|
||||
normalizeMs: Number(remoteTimings.normalizeMs || 0),
|
||||
},
|
||||
downloadStartedAt,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2530,13 +2795,16 @@ export async function download(chatId, options = {}) {
|
||||
);
|
||||
const remoteRevision = normalizeRevision(remoteSnapshot.meta.revision);
|
||||
|
||||
const importStartedAt = readSyncTimingNow();
|
||||
await db.importSnapshot(remoteSnapshot, {
|
||||
mode: "replace",
|
||||
preserveRevision: true,
|
||||
revision: remoteRevision,
|
||||
markSyncDirty: false,
|
||||
});
|
||||
const importMs = readSyncTimingNow() - importStartedAt;
|
||||
|
||||
const metaPatchStartedAt = readSyncTimingNow();
|
||||
await patchDbMeta(db, {
|
||||
deviceId: getOrCreateDeviceId(),
|
||||
lastSyncDownloadedAt: Date.now(),
|
||||
@@ -2545,12 +2813,15 @@ export async function download(chatId, options = {}) {
|
||||
syncDirtyReason: "",
|
||||
remoteSyncFormatVersion: BME_REMOTE_SYNC_FORMAT_VERSION_V2,
|
||||
});
|
||||
const metaPatchMs = readSyncTimingNow() - metaPatchStartedAt;
|
||||
|
||||
const hookStartedAt = readSyncTimingNow();
|
||||
await invokeSyncAppliedHook(options, {
|
||||
chatId: normalizedChatId,
|
||||
action: "download",
|
||||
revision: remoteRevision,
|
||||
});
|
||||
const hookMs = readSyncTimingNow() - hookStartedAt;
|
||||
|
||||
return {
|
||||
downloaded: true,
|
||||
@@ -2558,6 +2829,19 @@ export async function download(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
filename: remoteResult.filename,
|
||||
revision: remoteRevision,
|
||||
timings: finalizeSyncTimings(
|
||||
{
|
||||
resolveCandidatesMs: Number(remoteTimings.resolveCandidatesMs || 0),
|
||||
networkMs: Number(remoteTimings.networkMs || 0),
|
||||
parseMs: Number(remoteTimings.parseMs || 0),
|
||||
chunkReadMs: Number(remoteTimings.chunkReadMs || 0),
|
||||
normalizeMs: Number(remoteTimings.normalizeMs || 0),
|
||||
importMs,
|
||||
metaPatchMs,
|
||||
hookMs,
|
||||
},
|
||||
downloadStartedAt,
|
||||
),
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn("[ST-BME] 下载同步文件失败:", error);
|
||||
@@ -2567,6 +2851,7 @@ export async function download(chatId, options = {}) {
|
||||
chatId: normalizedChatId,
|
||||
reason: "download-error",
|
||||
error,
|
||||
timings: finalizeSyncTimings({}, downloadStartedAt),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user