Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions .vitepress/scripts/check-share-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import path from "node:path";
import { checkShareLinkFiles } from "../utilities/share-link-registry-files.ts";

const projectRoot = path.resolve(import.meta.dirname, "../..");
const registryFile = path.join(projectRoot, ".vitepress/data/share-links.json");
const pageIds = await fg("posts/**/*.md", {
cwd: projectRoot,
onlyFiles: true,
});
const result = await checkShareLinkFiles({ registryFile, pageIds });
const result = await checkShareLinkFiles({ pageIds });

console.info(
`[share-links] ${result.activeCount} active, ${result.goneCount} gone; registry check passed`,
`[share-links] ${result.activeCount} deterministic IDs; generation check passed`,
);
4 changes: 1 addition & 3 deletions .vitepress/scripts/prepare-share-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import path from "node:path";
import { prepareShareLinkFiles } from "../utilities/share-link-registry-files.ts";

const projectRoot = path.resolve(import.meta.dirname, "../..");
const baseRegistryFile = path.join(projectRoot, ".vitepress/data/share-links.json");
const generatedRegistryFile = path.join(
projectRoot,
".vitepress/generated/share-links.json",
Expand All @@ -18,12 +17,11 @@ const pageIds = await fg("posts/**/*.md", {
onlyFiles: true,
});
const result = await prepareShareLinkFiles({
baseRegistryFile,
generatedRegistryFile,
generatedManifestFile,
pageIds,
});

console.info(
`[share-links] ${result.added.length} added, ${result.unchangedCount} retained, ${result.generatedRegistryChanged ? "generated registry updated" : "generated registry unchanged"}, ${result.manifestChanged ? "manifest updated" : "manifest unchanged"}`,
`[share-links] ${result.generatedCount} generated, ${result.generatedRegistryChanged ? "registry updated" : "registry unchanged"}, ${result.manifestChanged ? "manifest updated" : "manifest unchanged"}`,
);
54 changes: 8 additions & 46 deletions .vitepress/utilities/share-link-registry-files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import {
} from "../shared/share-link-contract.ts";
import {
createShareLinkIndex,
isShareId,
prepareShareLinks,
generateShareLinks,
resolveCanonicalHref,
validateShareLinkRegistry,
type PrepareShareLinksResult,
type GenerateShareLinksResult,
type ShareId,
type ShareLinkRegistry,
} from "./share-links.ts";
Expand All @@ -22,24 +20,21 @@ export {
} from "../shared/share-link-contract.ts";

export interface ShareLinkFilePaths {
baseRegistryFile: string;
generatedRegistryFile: string;
generatedManifestFile: string;
}

export interface PrepareShareLinkFilesInput extends ShareLinkFilePaths {
pageIds: Iterable<string>;
readTextIfExists?: (file: string) => Promise<string | undefined>;
}

export interface PrepareShareLinkFilesResult extends PrepareShareLinksResult {
export interface PrepareShareLinkFilesResult extends GenerateShareLinksResult {
generatedRegistryChanged: boolean;
manifestChanged: boolean;
manifest: ShareLinkManifest;
}

export interface CheckShareLinkFilesInput {
registryFile: string;
pageIds: Iterable<string>;
}

Expand Down Expand Up @@ -83,14 +78,13 @@ async function readTextIfExists(file: string): Promise<string | undefined> {
export async function loadShareLinkRegistry(
registryFile: string,
): Promise<ShareLinkRegistry> {
return loadShareLinkRegistryFromReader(registryFile, readTextIfExists);
return loadShareLinkRegistryFromReader(registryFile);
}

async function loadShareLinkRegistryFromReader(
registryFile: string,
readText: (file: string) => Promise<string | undefined>,
): Promise<ShareLinkRegistry> {
const content = await readText(registryFile);
const content = await readTextIfExists(registryFile);

if (content === undefined) return defaultRegistry();

Expand Down Expand Up @@ -196,27 +190,9 @@ export async function prepareShareLinkFiles(
const releaseLock = await acquireRegistryLock(input.generatedRegistryFile);

try {
const readText = input.readTextIfExists ?? readTextIfExists;
const initialRegistryContent = await readText(input.baseRegistryFile);
const initialRegistryHash = hashContent(initialRegistryContent ?? "");
const existingRegistry = await loadShareLinkRegistryFromReader(
input.baseRegistryFile,
readText,
);
const prepared = prepareShareLinks({
registry: existingRegistry,
pageIds: input.pageIds,
});
const prepared = generateShareLinks({ pageIds: input.pageIds });
const registryContent = serializeJson(prepared.registry);

const beforeWriteRegistryContent = await readText(input.baseRegistryFile);

if (hashContent(beforeWriteRegistryContent ?? "") !== initialRegistryHash) {
throw new Error(
`基础分享注册表在 prepare 期间已被修改,拒绝生成:${input.baseRegistryFile}`,
);
}

const generatedRegistryChanged = await writeTextAtomicallyIfChanged(
input.generatedRegistryFile,
registryContent,
Expand Down Expand Up @@ -244,20 +220,6 @@ export async function prepareShareLinkFiles(
export async function checkShareLinkFiles(
input: CheckShareLinkFilesInput,
): Promise<CheckShareLinkFilesResult> {
const registry = await loadShareLinkRegistry(input.registryFile);
validateShareLinkRegistry(registry, input.pageIds);

let activeCount = 0;
let goneCount = 0;

for (const [id, record] of Object.entries(registry.records)) {
if (!isShareId(id)) {
throw new Error(`非法分享 ID:${id}`);
}

if (record.status === "active") activeCount += 1;
if (record.status === "gone") goneCount += 1;
}

return { activeCount, goneCount };
const { generatedCount } = generateShareLinks({ pageIds: input.pageIds });
return { activeCount: generatedCount, goneCount: 0 };
}
68 changes: 14 additions & 54 deletions .vitepress/utilities/share-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export {
isShareId,
type ShareId,
} from "../shared/share-link-contract.ts";
export const SHARE_ID_MAX_ATTEMPTS = 1024;

const SHARE_ID_SPACE = BigInt(SHARE_ID_ALPHABET.length) ** BigInt(SHARE_ID_LENGTH);
export type ShareLinkStatus = "active" | "gone";
Expand All @@ -33,10 +32,9 @@ export interface ShareLinkIndex {
byPageId: ReadonlyMap<string, ShareId>;
}

export interface PrepareShareLinksResult {
export interface GenerateShareLinksResult {
registry: ShareLinkRegistry;
added: ReadonlyArray<{ id: ShareId; pageId: string }>;
unchangedCount: number;
generatedCount: number;
}

export function normalizeSharePageId(pageId: string): string {
Expand All @@ -59,12 +57,6 @@ function compareStrings(left: string, right: string): number {
return 0;
}

function assertSafeAttempt(attempt: number): void {
if (!Number.isSafeInteger(attempt) || attempt < 0) {
throw new Error(`短 ID attempt 必须是非负安全整数:${attempt}`);
}
}

function encodeFixedBase31(value: bigint): ShareId {
const radix = BigInt(SHARE_ID_ALPHABET.length);
const output = new Array<string>(SHARE_ID_LENGTH);
Expand All @@ -82,15 +74,12 @@ function encodeFixedBase31(value: bigint): ShareId {
return output.join("");
}

export function generateStaticShareId(input: {
pageId: string;
attempt: number;
}): ShareId {
const pageId = normalizeSharePageId(input.pageId);
assertSafeAttempt(input.attempt);
export function generateStaticShareId(pageIdInput: string): ShareId {
const pageId = normalizeSharePageId(pageIdInput);

const digest = createHash("sha256")
.update(`yuufrag-share-v1\0${pageId}\0${input.attempt}`, "utf8")
// Keep the former attempt=0 payload so existing paths retain their IDs.
.update(`yuufrag-share-v1\0${pageId}\0${0}`, "utf8")
.digest("hex");
const value = BigInt(`0x${digest}`) % SHARE_ID_SPACE;

Expand Down Expand Up @@ -228,60 +217,31 @@ function createRegistryCopy(registry: ShareLinkRegistry): ShareLinkRegistry {
return { version: 1, records };
}

export function prepareShareLinks(input: {
registry: ShareLinkRegistry;
export function generateShareLinks(input: {
pageIds: Iterable<string>;
}): PrepareShareLinksResult {
}): GenerateShareLinksResult {
const currentPageIds = normalizeCurrentPageIds(input.pageIds);
const existingIndex = createShareLinkIndex(input.registry);
const currentPageIdSet = new Set(currentPageIds);

for (const [pageId, shareId] of existingIndex.byPageId) {
if (!currentPageIdSet.has(pageId)) {
throw new Error(
`active 分享 ID 指向不存在页面:${shareId} -> ${pageId}`,
);
}
}

const registry = createRegistryCopy(input.registry);
const claimedIds = new Set<ShareId>(existingIndex.byId.keys());
const assignedPageIds = new Set<string>(existingIndex.byPageId.keys());
const added: Array<{ id: ShareId; pageId: string }> = [];
const registry: ShareLinkRegistry = { version: 1, records: {} };

for (const pageId of currentPageIds) {
if (assignedPageIds.has(pageId)) continue;

let shareId: ShareId | undefined;

for (let attempt = 0; attempt < SHARE_ID_MAX_ATTEMPTS; attempt += 1) {
const candidate = generateStaticShareId({ pageId, attempt });

if (claimedIds.has(candidate)) continue;

shareId = candidate;
break;
}
const shareId = generateStaticShareId(pageId);
const existingRecord = registry.records[shareId];

if (shareId === undefined) {
if (existingRecord !== undefined) {
throw new Error(
` ID 分配超过最大尝试次数:${pageId}(${SHARE_ID_MAX_ATTEMPTS})`,
`确定性短 ID 碰撞:${shareId}\n- ${existingRecord.pageId}\n- ${pageId}`,
);
}

claimedIds.add(shareId);
assignedPageIds.add(pageId);
registry.records[shareId] = { pageId, status: "active" };
added.push({ id: shareId, pageId });
}

const sortedRegistry = createRegistryCopy(registry);
validateShareLinkRegistry(sortedRegistry, currentPageIds);

return {
registry: sortedRegistry,
added,
unchangedCount: currentPageIds.length - added.length,
generatedCount: currentPageIds.length,
};
}

Expand Down
Loading
Loading