Files
ttstd-infinite-canvas/web/src/lib/asset-storage-revision.ts

139 lines
5.6 KiB
TypeScript

import type { Asset } from "@/stores/use-asset-store";
export type AssetStorageDocument = {
state: { assets: Asset[] };
version: number;
storageRevision: number;
tombstones: { assets: Record<string, number> };
};
export function parseAssetStorageDocument(value: string | null, fallback: Asset[] = []): AssetStorageDocument {
if (!value) {
return {
state: { assets: fallback },
version: 0,
storageRevision: 0,
tombstones: { assets: {} },
};
}
const parsed = JSON.parse(value) as {
state?: { assets?: unknown };
version?: unknown;
storageRevision?: unknown;
tombstones?: { assets?: unknown };
};
if (!Array.isArray(parsed.state?.assets)) throw new Error("素材持久状态无效");
const rawTombstones = parsed.tombstones?.assets;
const tombstones =
rawTombstones && typeof rawTombstones === "object" && !Array.isArray(rawTombstones) ? Object.fromEntries(Object.entries(rawTombstones).filter((entry): entry is [string, number] => typeof entry[1] === "number" && Number.isFinite(entry[1]))) : {};
return {
state: { assets: parsed.state.assets as Asset[] },
version: typeof parsed.version === "number" ? parsed.version : 0,
storageRevision: typeof parsed.storageRevision === "number" && Number.isFinite(parsed.storageRevision) ? parsed.storageRevision : 0,
tombstones: { assets: tombstones },
};
}
export function serializeAssetStorageDocument(document: AssetStorageDocument) {
return JSON.stringify(document);
}
function deepEqual(left: unknown, right: unknown) {
if (Object.is(left, right)) return true;
return JSON.stringify(left) === JSON.stringify(right);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function mergeValue(base: unknown, local: unknown, durable: unknown): unknown {
if (deepEqual(local, base)) return durable;
if (deepEqual(durable, base)) return local;
if (isRecord(base) && isRecord(local) && isRecord(durable)) return mergeRecord(base, local, durable);
if (!isRecord(base) && isRecord(local) && isRecord(durable)) return mergeRecord({}, local, durable);
return durable;
}
function mergeRecord(base: Record<string, unknown>, local: Record<string, unknown>, durable: Record<string, unknown>) {
const merged: Record<string, unknown> = {};
for (const key of new Set([...Object.keys(base), ...Object.keys(local), ...Object.keys(durable)])) {
const baseHas = Object.prototype.hasOwnProperty.call(base, key);
const localHas = Object.prototype.hasOwnProperty.call(local, key);
const durableHas = Object.prototype.hasOwnProperty.call(durable, key);
if (!localHas && baseHas) {
if (durableHas && !deepEqual(durable[key], base[key])) merged[key] = durable[key];
continue;
}
if (!localHas) {
if (durableHas) merged[key] = durable[key];
continue;
}
if (!baseHas) {
merged[key] = durableHas ? mergeValue(undefined, local[key], durable[key]) : local[key];
continue;
}
if (!durableHas) {
if (!deepEqual(local[key], base[key])) merged[key] = local[key];
continue;
}
merged[key] = mergeValue(base[key], local[key], durable[key]);
}
return merged;
}
export function rebaseAssetSnapshot(input: { document: AssetStorageDocument; baseAssets: Asset[]; localAssets: Asset[]; baseRevision: number }) {
const nextRevision = input.document.storageRevision + 1;
const tombstones = { assets: { ...input.document.tombstones.assets } };
const baseById = new Map(input.baseAssets.map((asset) => [asset.id, asset]));
const localById = new Map(input.localAssets.map((asset) => [asset.id, asset]));
const durableById = new Map(input.document.state.assets.map((asset) => [asset.id, asset]));
const assets = [...input.document.state.assets];
const positions = new Map(assets.map((asset, index) => [asset.id, index]));
const remove = (id: string) => {
const index = positions.get(id);
if (index === undefined) return;
assets.splice(index, 1);
positions.clear();
assets.forEach((asset, position) => positions.set(asset.id, position));
};
const set = (asset: Asset) => {
const index = positions.get(asset.id);
if (index === undefined) {
positions.set(asset.id, assets.length);
assets.push(asset);
} else {
assets[index] = asset;
}
};
for (const id of new Set([...baseById.keys(), ...localById.keys()])) {
const base = baseById.get(id);
const local = localById.get(id);
const durable = durableById.get(id);
if (base && !local) {
remove(id);
tombstones.assets[id] = nextRevision;
continue;
}
if (!local || (base && deepEqual(base, local))) continue;
if (!durable) {
if (base || (tombstones.assets[id] ?? 0) > input.baseRevision) continue;
delete tombstones.assets[id];
set(local);
continue;
}
delete tombstones.assets[id];
set(mergeRecord((base || {}) as unknown as Record<string, unknown>, local as unknown as Record<string, unknown>, durable as unknown as Record<string, unknown>) as unknown as Asset);
}
return {
state: { assets },
version: input.document.version,
storageRevision: nextRevision,
tombstones,
} satisfies AssetStorageDocument;
}