main code

This commit is contained in:
Akshat Mehta
2025-11-23 11:03:57 +05:30
parent 0737ead33d
commit edd5728428
705 changed files with 80627 additions and 0 deletions

168
shared/utils/src/history.ts Normal file
View File

@@ -0,0 +1,168 @@
import type { Logger, LoggerFactory } from '@amp/web-apps-logger';
import { LruMap } from './lru-map';
import type { ScrollableElement } from './try-scroll';
import { tryScroll } from './try-scroll';
import { removeHost } from './url';
import { generateUuid } from './uuid';
export interface Options {
getScrollablePageElement(): ScrollableElement | null;
}
type Id = string;
const HISTORY_SIZE_LIMIT = 10;
interface WithScrollPosition<State> {
scrollY: number;
state: State;
}
/**
* We are using a currentStateId on this class to always store the state id instead of saving
* it on the window.history.state because there seems to be a bug in Safari where it is mutating
* the window.history.state to null after our Sign In flow which includes multiple iframes
* and multiple internal state changes inside the iframes. We can move back to window.history.state storing the id
* if the Safari Issue is fixed in future.
*/
export class History<State> {
private readonly log: Logger;
private readonly states: LruMap<Id, WithScrollPosition<State>>;
private readonly getScrollablePageElement: () => ScrollableElement | null;
private currentStateId: string | undefined;
constructor(
loggerFactory: LoggerFactory,
options: Options,
sizeLimit: number = HISTORY_SIZE_LIMIT,
) {
this.log = loggerFactory.loggerFor('History');
this.states = new LruMap(sizeLimit);
this.getScrollablePageElement = options.getScrollablePageElement;
}
// Update page data but keep scroll position
updateState(update: (state?: State) => State): void {
if (!this.currentStateId) {
this.log.warn(
'failed: encountered a null currentStateId inside updateState',
);
return;
}
const currentState = this.states.get(this.currentStateId);
const newState = update(currentState?.state);
this.log.info('updateState', newState, this.currentStateId);
this.states.set(this.currentStateId, {
...(currentState as WithScrollPosition<State>),
state: newState,
});
}
replaceState(state: State, url: string | null): void {
const id = generateId();
this.log.info('replaceState', state, url, id);
window.history.replaceState({ id }, '', this.removeHost(url));
this.currentStateId = id;
this.states.set(id, { state, scrollY: 0 });
this.scrollTop = 0;
}
pushState(state: State, url: string | null): void {
const id = generateId();
this.log.info('pushState', state, url, id);
window.history.pushState({ id }, '', this.removeHost(url));
this.currentStateId = id;
this.states.set(id, { state, scrollY: 0 });
this.scrollTop = 0;
}
beforeTransition(): void {
const { state } = window.history;
if (!state) {
return;
}
const oldState = this.states.get(state.id);
if (!oldState) {
this.log.info(
'current history state evicted from LRU, not saving scroll position',
);
return;
}
const { scrollTop } = this;
this.states.set(state.id, {
...oldState,
scrollY: scrollTop,
});
this.log.info('saving scroll position', scrollTop);
}
private removeHost(url: string | null): string | undefined {
if (!url) {
this.log.warn('received null URL');
return;
}
// TODO: rdar://77982655 (Investigate router improvements): host mismatch?
return removeHost(url);
}
onPopState(
listener: (url: string, state: State | undefined) => void,
): void {
window.addEventListener('popstate', (event: PopStateEvent): void => {
this.currentStateId = event.state?.id;
if (!this.currentStateId) {
this.log.warn(
'encountered a null event.state.id in onPopState event: ',
window.location.href,
);
}
this.log.info('popstate', this.states, this.currentStateId);
const state = this.currentStateId
? this.states.get(this.currentStateId)
: undefined;
listener(window.location.href, state?.state);
if (!state) {
return;
}
const { scrollY } = state;
this.log.info('restoring scroll to', scrollY);
tryScroll(this.log, () => this.getScrollablePageElement(), scrollY);
});
}
private get scrollTop(): number {
return this.getScrollablePageElement()?.scrollTop || 0;
}
private set scrollTop(scrollTop: number) {
const element = this.getScrollablePageElement();
if (element) {
element.scrollTop = scrollTop;
}
}
// TODO: rdar://77982655 (Investigate router improvements): offPopState?
}
/**
* Generate a (unique) id for storing in window.history.state.
*
* @return the generated ID
*/
function generateId(): Id {
// The use of something random (and not say, an incrementing counter) is important
// here. These states can survive refreshes so the IDs used must be globally unique
// (and not just unique to the current page load).
return generateUuid();
}