atagen

malice

public
Star0
Watch0
bf301cf5de2f45d2453bb5cc8a28d8991b2c4cb5
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/back-stack.ts

0e50e0d137afatagen2026-10-26 05:50 UTC1
const KEY = 'backLayer';
0e50e0d137afatagen2026-10-26 05:50 UTC2
const SETTLE_MS = 400;
0e50e0d137afatagen2026-10-26 05:50 UTC3
0e50e0d137afatagen2026-10-26 05:50 UTC4
export interface HistoryLike {
0e50e0d137afatagen2026-10-26 05:50 UTC5
  state: unknown;
0e50e0d137afatagen2026-10-26 05:50 UTC6
  pushState(state: unknown, unused: string, url?: string): void;
0e50e0d137afatagen2026-10-26 05:50 UTC7
  replaceState(state: unknown, unused: string, url?: string): void;
0e50e0d137afatagen2026-10-26 05:50 UTC8
  back(): void;
0e50e0d137afatagen2026-10-26 05:50 UTC9
}
0e50e0d137afatagen2026-10-26 05:50 UTC10
0e50e0d137afatagen2026-10-26 05:50 UTC11
interface Layer {
0e50e0d137afatagen2026-10-26 05:50 UTC12
  id: string;
0e50e0d137afatagen2026-10-26 05:50 UTC13
  close: (id: string) => void;
0e50e0d137afatagen2026-10-26 05:50 UTC14
}
0e50e0d137afatagen2026-10-26 05:50 UTC15
0e50e0d137afatagen2026-10-26 05:50 UTC16
function layerOf(state: unknown): string | undefined {
0e50e0d137afatagen2026-10-26 05:50 UTC17
  if (state && typeof state === 'object' && KEY in state) return String((state as Record<string, unknown>)[KEY]);
0e50e0d137afatagen2026-10-26 05:50 UTC18
  return undefined;
0e50e0d137afatagen2026-10-26 05:50 UTC19
}
0e50e0d137afatagen2026-10-26 05:50 UTC20
0e50e0d137afatagen2026-10-26 05:50 UTC21
function plain(state: unknown): Record<string, unknown> {
0e50e0d137afatagen2026-10-26 05:50 UTC22
  const { [KEY]: _drop, ...rest } = (state && typeof state === 'object' ? state : {}) as Record<string, unknown>;
0e50e0d137afatagen2026-10-26 05:50 UTC23
  return rest;
0e50e0d137afatagen2026-10-26 05:50 UTC24
}
0e50e0d137afatagen2026-10-26 05:50 UTC25
0e50e0d137afatagen2026-10-26 05:50 UTC26
export class BackStack {
0e50e0d137afatagen2026-10-26 05:50 UTC27
  private stack: Layer[] = [];
0e50e0d137afatagen2026-10-26 05:50 UTC28
  private queue: Array<() => void> = [];
0e50e0d137afatagen2026-10-26 05:50 UTC29
  private waiting = false;
0e50e0d137afatagen2026-10-26 05:50 UTC30
  private timer: ReturnType<typeof setTimeout> | null = null;
0e50e0d137afatagen2026-10-26 05:50 UTC31
0e50e0d137afatagen2026-10-26 05:50 UTC32
  constructor(private readonly history: HistoryLike) {}
0e50e0d137afatagen2026-10-26 05:50 UTC33
0e50e0d137afatagen2026-10-26 05:50 UTC34
  /** A reload can land on a layer entry whose layer no longer exists. */
0e50e0d137afatagen2026-10-26 05:50 UTC35
  init(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC36
    if (layerOf(this.history.state) !== undefined) this.history.replaceState(plain(this.history.state), '');
0e50e0d137afatagen2026-10-26 05:50 UTC37
  }
0e50e0d137afatagen2026-10-26 05:50 UTC38
0e50e0d137afatagen2026-10-26 05:50 UTC39
  get depth(): number {
0e50e0d137afatagen2026-10-26 05:50 UTC40
    return this.stack.length;
0e50e0d137afatagen2026-10-26 05:50 UTC41
  }
0e50e0d137afatagen2026-10-26 05:50 UTC42
0e50e0d137afatagen2026-10-26 05:50 UTC43
  sync(id: string, open: boolean, close: (id: string) => void): void {
0e50e0d137afatagen2026-10-26 05:50 UTC44
    const index = this.stack.findIndex((l) => l.id === id);
0e50e0d137afatagen2026-10-26 05:50 UTC45
    if (open && index < 0) {
0e50e0d137afatagen2026-10-26 05:50 UTC46
      this.stack.push({ id, close });
0e50e0d137afatagen2026-10-26 05:50 UTC47
      this.run(() => this.history.pushState({ ...plain(this.history.state), [KEY]: id }, ''));
0e50e0d137afatagen2026-10-26 05:50 UTC48
    } else if (open) {
0e50e0d137afatagen2026-10-26 05:50 UTC49
      this.stack[index].close = close;
0e50e0d137afatagen2026-10-26 05:50 UTC50
    } else if (index >= 0) {
0e50e0d137afatagen2026-10-26 05:50 UTC51
      const top = index === this.stack.length - 1;
0e50e0d137afatagen2026-10-26 05:50 UTC52
      this.stack.splice(index, 1);
0e50e0d137afatagen2026-10-26 05:50 UTC53
      if (top) this.run(() => this.rewind());
0e50e0d137afatagen2026-10-26 05:50 UTC54
    }
0e50e0d137afatagen2026-10-26 05:50 UTC55
  }
0e50e0d137afatagen2026-10-26 05:50 UTC56
0e50e0d137afatagen2026-10-26 05:50 UTC57
  /** Records a route, replacing the layer entry on top when layers are being dismissed by the navigation. */
0e50e0d137afatagen2026-10-26 05:50 UTC58
  route(state: unknown, url: string): void {
0e50e0d137afatagen2026-10-26 05:50 UTC59
    this.run(() => {
0e50e0d137afatagen2026-10-26 05:50 UTC60
      if (this.stack.length > 0 && layerOf(this.history.state) !== undefined) {
0e50e0d137afatagen2026-10-26 05:50 UTC61
        this.stack = [];
0e50e0d137afatagen2026-10-26 05:50 UTC62
        this.history.replaceState(state, '', url);
0e50e0d137afatagen2026-10-26 05:50 UTC63
      } else {
0e50e0d137afatagen2026-10-26 05:50 UTC64
        this.history.pushState(state, '', url);
0e50e0d137afatagen2026-10-26 05:50 UTC65
      }
0e50e0d137afatagen2026-10-26 05:50 UTC66
    });
0e50e0d137afatagen2026-10-26 05:50 UTC67
  }
0e50e0d137afatagen2026-10-26 05:50 UTC68
0e50e0d137afatagen2026-10-26 05:50 UTC69
  /** Returns true when the popstate belonged to the layer stack, false when it is a plain route change. */
0e50e0d137afatagen2026-10-26 05:50 UTC70
  pop(state: unknown): boolean {
0e50e0d137afatagen2026-10-26 05:50 UTC71
    if (this.waiting) {
0e50e0d137afatagen2026-10-26 05:50 UTC72
      this.settle();
0e50e0d137afatagen2026-10-26 05:50 UTC73
      return true;
0e50e0d137afatagen2026-10-26 05:50 UTC74
    }
0e50e0d137afatagen2026-10-26 05:50 UTC75
    const id = layerOf(state);
0e50e0d137afatagen2026-10-26 05:50 UTC76
    if (id === undefined) {
0e50e0d137afatagen2026-10-26 05:50 UTC77
      if (this.stack.length === 0) return false;
0e50e0d137afatagen2026-10-26 05:50 UTC78
      this.closeAbove(-1);
0e50e0d137afatagen2026-10-26 05:50 UTC79
      return true;
0e50e0d137afatagen2026-10-26 05:50 UTC80
    }
0e50e0d137afatagen2026-10-26 05:50 UTC81
    const index = this.stack.findIndex((l) => l.id === id);
0e50e0d137afatagen2026-10-26 05:50 UTC82
    if (index < 0) {
0e50e0d137afatagen2026-10-26 05:50 UTC83
      this.history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC84
      return true;
0e50e0d137afatagen2026-10-26 05:50 UTC85
    }
0e50e0d137afatagen2026-10-26 05:50 UTC86
    this.closeAbove(index);
0e50e0d137afatagen2026-10-26 05:50 UTC87
    return true;
0e50e0d137afatagen2026-10-26 05:50 UTC88
  }
0e50e0d137afatagen2026-10-26 05:50 UTC89
0e50e0d137afatagen2026-10-26 05:50 UTC90
  private closeAbove(index: number): void {
0e50e0d137afatagen2026-10-26 05:50 UTC91
    const closing = this.stack.splice(index + 1);
0e50e0d137afatagen2026-10-26 05:50 UTC92
    for (const layer of closing.reverse()) layer.close(layer.id);
0e50e0d137afatagen2026-10-26 05:50 UTC93
  }
0e50e0d137afatagen2026-10-26 05:50 UTC94
0e50e0d137afatagen2026-10-26 05:50 UTC95
  private rewind(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC96
    if (layerOf(this.history.state) === undefined) return;
0e50e0d137afatagen2026-10-26 05:50 UTC97
    this.waiting = true;
0e50e0d137afatagen2026-10-26 05:50 UTC98
    this.timer = setTimeout(() => this.settle(), SETTLE_MS);
0e50e0d137afatagen2026-10-26 05:50 UTC99
    this.history.back();
0e50e0d137afatagen2026-10-26 05:50 UTC100
  }
0e50e0d137afatagen2026-10-26 05:50 UTC101
0e50e0d137afatagen2026-10-26 05:50 UTC102
  private settle(): void {
0e50e0d137afatagen2026-10-26 05:50 UTC103
    this.waiting = false;
0e50e0d137afatagen2026-10-26 05:50 UTC104
    if (this.timer !== null) clearTimeout(this.timer);
0e50e0d137afatagen2026-10-26 05:50 UTC105
    this.timer = null;
0e50e0d137afatagen2026-10-26 05:50 UTC106
    const next = this.queue.shift();
0e50e0d137afatagen2026-10-26 05:50 UTC107
    if (next) this.run(next);
0e50e0d137afatagen2026-10-26 05:50 UTC108
  }
0e50e0d137afatagen2026-10-26 05:50 UTC109
0e50e0d137afatagen2026-10-26 05:50 UTC110
  private run(op: () => void): void {
0e50e0d137afatagen2026-10-26 05:50 UTC111
    if (this.waiting) {
0e50e0d137afatagen2026-10-26 05:50 UTC112
      this.queue.push(op);
0e50e0d137afatagen2026-10-26 05:50 UTC113
      return;
0e50e0d137afatagen2026-10-26 05:50 UTC114
    }
0e50e0d137afatagen2026-10-26 05:50 UTC115
    op();
0e50e0d137afatagen2026-10-26 05:50 UTC116
    if (!this.waiting) {
0e50e0d137afatagen2026-10-26 05:50 UTC117
      const next = this.queue.shift();
0e50e0d137afatagen2026-10-26 05:50 UTC118
      if (next) this.run(next);
0e50e0d137afatagen2026-10-26 05:50 UTC119
    }
0e50e0d137afatagen2026-10-26 05:50 UTC120
  }
0e50e0d137afatagen2026-10-26 05:50 UTC121
}
0e50e0d137afatagen2026-10-26 05:50 UTC122
0e50e0d137afatagen2026-10-26 05:50 UTC123
export const backStack = new BackStack(typeof window === 'undefined' ? ({} as HistoryLike) : window.history);
0e50e0d137afatagen2026-10-26 05:50 UTC124
0e50e0d137afatagen2026-10-26 05:50 UTC125
let instances = 0;
0e50e0d137afatagen2026-10-26 05:50 UTC126
0e50e0d137afatagen2026-10-26 05:50 UTC127
export function instanceLayerId(prefix: string): string {
0e50e0d137afatagen2026-10-26 05:50 UTC128
  return `${prefix}-${++instances}`;
0e50e0d137afatagen2026-10-26 05:50 UTC129
}