Files
wehub-resource-sync 2114b14ee0
Sync main into demo / sync (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:35:26 +08:00

113 lines
3.8 KiB
TypeScript

/**
* Shadow history tracker for MemoryRouter.
*
* react-router-dom v7 的 MemoryHistory 不再暴露 `entries` 数组(只有 `index` getter),
* 导致无法从 navigator 上直接读取历史栈。本模块维护一份与 MemoryHistory 同步的影子栈,
* 供 `memoryHistoryPopTo` 在栈中搜索目标路径并计算 go(-delta)。
*
* 同步方式:通过 useEffect 监听 location 变化,利用 navigator.index(可靠可用)
* 判断 push / replace / pop,更新影子栈。
*/
interface StackEntry {
pathname: string;
search: string;
}
export class HistoryTracker {
stack: StackEntry[];
index: number;
constructor(initial: StackEntry, initialIndex: number) {
// If the tracker is created when navigator.index > 0 (e.g. App mounts
// after several routes have been pushed), entries before initialIndex
// are unknown — they're filled with empty placeholders that findPopToDelta
// will skip. These slots get populated if the user navigates back through
// them (the POP branch in sync() overwrites with real location data).
this.stack = [];
for (let i = 0; i < initialIndex; i++) {
this.stack.push({ pathname: '', search: '' });
}
this.stack.push(initial);
this.index = initialIndex;
}
sync(location: StackEntry, navIndex: number) {
if (navIndex > this.index) {
// PUSH — trim forward entries, append new
this.stack.length = this.index + 1;
while (this.stack.length < navIndex) {
this.stack.push({ pathname: '', search: '' });
}
this.stack.push(location);
this.index = navIndex;
} else if (navIndex === this.index) {
// REPLACE — update entry at current index
this.stack[this.index] = location;
} else {
// POP / go(-n). Overwrite with current location as a self-healing
// measure: normally the value matches the original entry, but if the
// tracker drifted (e.g. timing edge case or placeholder slot), this
// corrects it with the authoritative location from React Router.
this.index = navIndex;
if (this.index >= 0 && this.index < this.stack.length) {
this.stack[this.index] = location;
}
}
}
/**
* Search backwards from current index for a matching pathname (or full path+search).
* Returns the number of steps to go() back; 0 = already there, -1 = not found.
*/
findPopToDelta(target: string, inclusive: boolean): number {
const wantFull = target.includes('?');
for (let i = this.index; i >= 0; i--) {
const entry = this.stack[i];
if (!entry || (!entry.pathname && !entry.search)) continue;
const full = entry.search
? `${entry.pathname}${entry.search}`
: entry.pathname;
const match = wantFull ? full === target : entry.pathname === target;
if (match) {
const baseIndex = inclusive ? Math.max(0, i - 1) : i;
return this.index - baseIndex;
}
}
return -1;
}
}
const trackers = new WeakMap<object, HistoryTracker>();
/**
* Sync the shadow tracker with the actual MemoryHistory state.
* Call this in a useEffect that depends on [navigator, location.key].
*/
export function syncTracker(
navigator: object,
location: { pathname: string; search: string },
): void {
const navIndex = (navigator as any).index;
if (typeof navIndex !== 'number') return;
let tracker = trackers.get(navigator);
if (!tracker) {
tracker = new HistoryTracker(
{ pathname: location.pathname, search: location.search || '' },
navIndex,
);
trackers.set(navigator, tracker);
return;
}
tracker.sync(
{ pathname: location.pathname, search: location.search || '' },
navIndex,
);
}
export function getTracker(navigator: object): HistoryTracker | undefined {
return trackers.get(navigator);
}