better code: use SSE for both simple list and search in front-end
Massimo Melina committed
Jan 1, 2022 at 17:53 UTC
82d86788ccf151fb9d55883d92f345457a24cfaf
5 files changed
+50
-56
frontend/src/BrowseFiles.ts
+1
-1
@@ -31,7 +31,7 @@ function FilesList() {
31
const filter = snap.listFilter > '' && new RegExp(_.escapeRegExp(snap.listFilter),'i')
32
let n = 0 // if I try to use directly the state as counter I get a "too many re-renders" error
33
const ret = h('ul', { className: 'dir' },
34
- !list.length ? (loading || 'Nothing here')
34
+ !list.length ? (!loading && 'Nothing here')
35
: list.map((entry: DirEntry) =>
36
h(File, { key: entry.n, hidden: filter && !filter.test(entry.n) || !++n, ...entry })),
37
loading && h(Spinner))
frontend/src/Head.ts
+10
-2
@@ -1,4 +1,4 @@
1
-import { createElement as h, Fragment, useContext, useMemo, useState } from 'react'
1
+import { createElement as h, Fragment, useContext, useEffect, useMemo, useState } from 'react'
2
import { Link, useLocation } from 'react-router-dom'
3
import { ListContext } from './BrowseFiles'
4
import { login, logout } from './login'
@@ -23,7 +23,15 @@ function MenuPanel() {
23
const [showFilter, setShowFilter] = useState(listFilter > '')
24
const [filter, setFilter] = useState(listFilter)
25
;[state.listFilter] = useDebounce(showFilter ? filter : '', 300)
26
- const searchButtonProps = stopSearch ? {
26
+
27
+ const [started1secAgo, setStarted1secAgo] = useState(false)
28
+ useEffect(()=>{
29
+ if (!stopSearch) return
30
+ setStarted1secAgo(false)
31
+ setTimeout(()=> setStarted1secAgo(true), 1000)
32
+ }, [stopSearch])
33
+
34
+ const searchButtonProps = stopSearch && started1secAgo ? {
35
icon: 'stop',
36
label: 'Stop list',
37
className: 'ani-working',
frontend/src/api.ts
+8
-2
@@ -41,8 +41,14 @@ export function useApi(cmd: string | Falsy, params?: object) : any {
41
42
type EventHandler = (type:string, data?:any) => void
43
44
-export function apiEvents(cmd: string, params: object, cb:EventHandler) {
45
- const source = new EventSource(PREFIX + cmd + '?' + new URLSearchParams(params as any))
44
+export function apiEvents(cmd: string, params: Record<string,any>, cb:EventHandler) {
45
+ const processed: Record<string,string> = {}
46
+ for (const k in params) {
47
+ const v = params[k]
48
+ if (v === undefined) continue
49
+ processed[k] = v === true ? '1' : v
50
+ }
51
+ const source = new EventSource(PREFIX + cmd + '?' + new URLSearchParams(processed))
52
source.onopen = () => cb('connected')
53
source.onerror = err => cb('error', err)
54
source.onmessage = ({ data }) => {
frontend/src/index.scss
+1
-1
@@ -58,7 +58,7 @@ header {
58
59
@keyframes blink {
60
0% {opacity: 1}
61
- 50% {opacity: 0.1}
61
+ 50% {opacity: 0.2}
62
}
63
@keyframes spin {
64
from { transform: rotate(0deg); }
frontend/src/useFetchList.ts
+30
-50
@@ -1,6 +1,6 @@
1
import { state, useSnapState } from './state'
2
import { useEffect, useRef, useState } from 'react'
3
-import { apiCall, apiEvents } from './api'
3
+import { apiEvents } from './api'
4
import { DirList, usePath } from './BrowseFiles'
5
6
export default function useFetchList() {
@@ -28,63 +28,43 @@ export default function useFetchList() {
28
29
;(async ()=>{
30
const API = 'file_list'
31
- const sse = search
32
- const baseParams = { path:desiredPath, search, sse, omit:'c' }
31
+ const baseParams = { path:desiredPath, search, sse:true, omit:'c' }
32
let list: DirList = []
33
setList(list)
34
setLoading(true)
35
setError(undefined)
36
38
- if (sse) { // buffering entries is necessary against burst of events that will hang the browser
39
- const buffer:DirList = []
40
- const flush = () => {
41
- const chunk = buffer.splice(0, Infinity)
42
- if (chunk.length)
43
- setList(list = [...list, ...chunk])
44
- }
45
- const timer = setInterval(flush, 1000)
46
- const src = apiEvents(API, baseParams, (type, data) => {
47
- switch (type) {
48
- case 'error':
49
- return setError(Error(JSON.stringify(data)))
50
- case 'closed':
51
- flush()
52
- state.stopSearch?.()
53
- return setLoading(false)
54
- case 'msg':
55
- if (src?.readyState === src?.CLOSED)
56
- return state.stopSearch?.()
57
- buffer.push(data.entry)
58
- }
59
- })
60
- state.stopSearch = ()=>{
61
- buffer.length = 0
62
- setLoading(false)
63
- clearInterval(timer)
64
- state.stopSearch = undefined
65
- src.close()
66
- }
67
- return
37
+ // buffering entries is necessary against burst of events that will hang the browser
38
+ const buffer: DirList = []
39
+ const flush = () => {
40
+ const chunk = buffer.splice(0, Infinity)
41
+ if (chunk.length)
42
+ setList(list = [...list, ...chunk])
43
}
69
-
70
- let offset = 0
71
- while (1) {
72
- const limit = list.length ? 1000 : 100
73
- const res = await apiCall(API, { ...baseParams, offset, limit }, { noModal:true }).catch(e => e)
74
- || Error()
75
- if (res instanceof Error)
76
- return setError(res)
77
- if (res.redirect) {
78
- window.history.back() // cancel last piece of navigation that brought us here, we'll replace it with the following
79
- loc.href = res.redirect
44
+ const timer = setInterval(flush, 1000)
45
+ const src = apiEvents(API, baseParams, (type, data) => {
46
+ switch (type) {
47
+ case 'error':
48
+ state.stopSearch?.()
49
+ return setError(Error(JSON.stringify(data)))
50
+ case 'closed':
51
+ flush()
52
+ state.stopSearch?.()
53
+ return setLoading(false)
54
+ case 'msg':
55
+ if (src?.readyState === src?.CLOSED)
56
+ return state.stopSearch?.()
57
+ buffer.push(data.entry)
58
}
81
- const chunk = res.list
82
- setList(list = [ ...list, ...chunk ])
83
- if (chunk.length < limit)
84
- break
85
- offset = list.length
59
+ })
60
+ state.stopSearch = ()=>{
61
+ buffer.length = 0
62
+ setLoading(false)
63
+ clearInterval(timer)
64
+ state.stopSearch = undefined
65
+ src.close()
66
}
87
- setLoading(false)
67
+
68
})()
69
}, [desiredPath, search, snap.username])
70
return { list, loading, error }