refactor(web): extract VirtualScrollManager base class

Extract a reusable VirtualScrollManager base class from TimelineManager
to enable code reuse for virtual scrolling behavior.

Changes:
- Add VirtualScrollManager base class with common scrolling logic
- Add ScrollSegment base class for month/segment abstraction
- Migrate TimelineMonth to extend ScrollSegment
- Migrate TimelineManager to extend VirtualScrollManager
- Move intersection/layout/load logic from internal/ to base classes
- Update components to work with new architecture
- Delete timeline-manager/internal/{intersection,layout,load}-support.ts

No functional changes - pure refactoring.
This commit is contained in:
midzelis
2025-10-17 13:49:05 +00:00
parent 928b69f415
commit f1da027a8c
18 changed files with 923 additions and 785 deletions
+15 -8
View File
@@ -1,3 +1,10 @@
export enum TaskStatus {
DONE,
WAITED,
CANCELED,
LOADED,
ERRORED,
}
export class CancellableTask {
cancelToken: AbortController | null = null;
cancellable: boolean = true;
@@ -32,18 +39,18 @@ export class CancellableTask {
async waitUntilCompletion() {
if (this.executed) {
return 'DONE';
return TaskStatus.DONE;
}
// if there is a cancel token, task is currently executing, so wait on the promise. If it
// isn't, then the task is in new state, it hasn't been loaded, nor has it been executed.
// in either case, we wait on the promise.
await this.complete;
return 'WAITED';
return TaskStatus.WAITED;
}
async execute<F extends (abortSignal: AbortSignal) => Promise<void>>(f: F, cancellable: boolean) {
if (this.executed) {
return 'DONE';
return TaskStatus.DONE;
}
// if promise is pending, wait on previous request instead.
@@ -54,7 +61,7 @@ export class CancellableTask {
this.cancellable = cancellable;
}
await this.complete;
return 'WAITED';
return TaskStatus.WAITED;
}
this.cancellable = cancellable;
const cancelToken = (this.cancelToken = new AbortController());
@@ -62,18 +69,18 @@ export class CancellableTask {
try {
await f(cancelToken.signal);
if (cancelToken.signal.aborted) {
return 'CANCELED';
return TaskStatus.CANCELED;
}
this.#transitionToExecuted();
return 'LOADED';
return TaskStatus.LOADED;
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).name === 'AbortError') {
// abort error is not treated as an error, but as a cancellation.
return 'CANCELED';
return TaskStatus.CANCELED;
}
this.#transitionToErrored(error);
return 'ERRORED';
return TaskStatus.ERRORED;
} finally {
this.cancelToken = null;
}