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 }