summaryrefslogtreecommitdiffhomepage
path: root/packages/app/src/context/layout-scroll.ts
blob: ef66eccd90417f2b3a458e4e4a0057c028b93844 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { createStore, produce } from "solid-js/store"

export type SessionScroll = {
  x: number
  y: number
}

type ScrollMap = Record<string, SessionScroll>

type Options = {
  debounceMs?: number
  getSnapshot: (sessionKey: string) => ScrollMap | undefined
  onFlush: (sessionKey: string, scroll: ScrollMap) => void
}

export function createScrollPersistence(opts: Options) {
  const wait = opts.debounceMs ?? 200
  const [cache, setCache] = createStore<Record<string, ScrollMap>>({})
  const dirty = new Set<string>()
  const timers = new Map<string, ReturnType<typeof setTimeout>>()

  function clone(input?: ScrollMap) {
    const out: ScrollMap = {}
    if (!input) return out

    for (const key of Object.keys(input)) {
      const pos = input[key]
      if (!pos) continue
      out[key] = { x: pos.x, y: pos.y }
    }

    return out
  }

  function seed(sessionKey: string) {
    const next = clone(opts.getSnapshot(sessionKey))
    const current = cache[sessionKey]
    if (!current) {
      setCache(sessionKey, next)
      return
    }

    if (Object.keys(current).length > 0) return
    if (Object.keys(next).length === 0) return
    setCache(sessionKey, next)
  }

  function scroll(sessionKey: string, tab: string) {
    seed(sessionKey)
    return cache[sessionKey]?.[tab] ?? opts.getSnapshot(sessionKey)?.[tab]
  }

  function schedule(sessionKey: string) {
    const prev = timers.get(sessionKey)
    if (prev) clearTimeout(prev)
    timers.set(
      sessionKey,
      setTimeout(() => flush(sessionKey), wait),
    )
  }

  function setScroll(sessionKey: string, tab: string, pos: SessionScroll) {
    seed(sessionKey)

    const prev = cache[sessionKey]?.[tab]
    if (prev?.x === pos.x && prev?.y === pos.y) return

    setCache(sessionKey, tab, { x: pos.x, y: pos.y })
    dirty.add(sessionKey)
    schedule(sessionKey)
  }

  function flush(sessionKey: string) {
    const timer = timers.get(sessionKey)
    if (timer) clearTimeout(timer)
    timers.delete(sessionKey)

    if (!dirty.has(sessionKey)) return
    dirty.delete(sessionKey)

    opts.onFlush(sessionKey, clone(cache[sessionKey]))
  }

  function flushAll() {
    const keys = Array.from(dirty)
    if (keys.length === 0) return

    for (const key of keys) {
      flush(key)
    }
  }

  function drop(keys: string[]) {
    if (keys.length === 0) return

    for (const key of keys) {
      const timer = timers.get(key)
      if (timer) clearTimeout(timer)
      timers.delete(key)
      dirty.delete(key)
    }

    setCache(
      produce((draft) => {
        for (const key of keys) {
          delete draft[key]
        }
      }),
    )
  }

  function dispose() {
    drop(Array.from(timers.keys()))
  }

  return {
    cache,
    drop,
    flush,
    flushAll,
    scroll,
    seed,
    setScroll,
    dispose,
  }
}