better code
Massimo Melina committed
Sep 15, 2023 at 21:17 UTC
8c8ed2dcaf6d2ad496235dd9e876a7b1daf3776f
12 files changed
+51
-42
admin/src/FileForm.ts
+1
-5
@@ -15,7 +15,7 @@ import {
15
} from '@hfs/mui-grid-form'
16
import { apiCall, useApiEx } from './api'
17
import {
18
- basename, Btn, formatBytes, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
18
+ basename, Btn, formatBytes, formatTimestamp, IconBtn, isEqualLax, LinkBtn, modifiedSx, newDialog, objSameKeys,
19
onlyTruthy, prefix, wikiLink
20
} from './misc'
21
import { reloadVfs, VfsNode, VfsPerms, Who } from './VfsPage'
@@ -152,10 +152,6 @@ function perm2word(perm: string) {
152
return word === 'read' ? 'download' : word
153
}
154
155
-function formatTimestamp(x: string) {
156
- return x ? new Date(x).toLocaleString() : '-'
157
-}
158
-
155
interface WhoFieldProps extends FieldProps<Who | undefined> {
156
accounts: Account[],
157
otherPerms: any[],
admin/src/OptionsPage.ts
+1
-1
@@ -156,7 +156,7 @@ export default function OptionsPage() {
156
{ k: 'k', label: "File mask", $width: 1, $column: {
157
renderCell: ({ value, id }: any) => h('code', {},
158
value,
159
- value === '*' && id < Object.keys(values.mime).length - 1
159
+ value === '*' && id < _.size(values.mime) - 1
160
&& iconTooltip(Warning, md("Mime with `*` should be the last, because first matching row applies"), {
161
color: 'warning.main', ml: 1
162
}))
shared/api.ts
+2
-2
@@ -10,7 +10,7 @@ const timeoutByApi: Dict = {
10
loginSrp1: 90, // support antibrute
11
update: 600, // download can be lengthy
12
get_nat: 10,
13
- get_status: 20 // can be lengthy on slow machines because of the find-process-on-busy-port feature
13
+ get_status: 20, // can be lengthy on slow machines because of the find-process-on-busy-port feature
14
}
15
16
interface ApiCallOptions {
@@ -84,7 +84,7 @@ export function useApi<T=any>(cmd: string | Falsy, params?: object) {
84
let req: undefined | ReturnType<typeof apiCall>
85
const wholePromise = wait(0) // postpone a bit, so that if it is aborted immediately, it is never really fired (happens mostly in dev mode)
86
.then(() => aborted ? undefined : req = apiCall<T>(cmd, params))
87
- .then(res => aborted || setData(res), err => aborted || setError(err))
87
+ .then(res => aborted || setData(res), err => aborted || setError(err) || setData(undefined))
88
.finally(() => loadingRef.current = reloadingRef.current = undefined)
89
loadingRef.current = Object.assign(wholePromise, {
90
abort() {
src/api.net.ts
+5
-11
@@ -10,19 +10,13 @@ import { getIps, getServerStatus } from './listen'
10
import { getProjectInfo } from './github'
11
import { httpString } from './util-http'
12
import { exec } from 'child_process'
13
-import { debounceAsync, MINUTE, repeat } from './misc'
13
+import { debounceAsync, HOUR, MINUTE, repeat } from './misc'
14
15
-const client = new Client({ timeout: 5_000 })
16
-const original = client.getGateway
15
+const client = new Client({ timeout: 4_000 })
16
+const originalMethod = client.getGateway
17
// other client methods call getGateway too, so this will ensure they reuse this same result
18
-client.getGateway = function getGatewayCaching() {
19
- const promise = original.apply(client)
20
- client.getGateway = () => promise // multiple callings = same job
21
- promise.then(() => console.debug('caching gateway'), // store in cache only if successful.
22
- ()=> client.getGateway = getGatewayCaching) // failed, try again
23
- return promise
24
-}
25
-client.getGateway()
18
+client.getGateway = debounceAsync(() => originalMethod.apply(client), 0, { retain: HOUR, retainFailure: 30_000 })
19
+client.getGateway().catch(() => {})
20
21
export let externalIp = Promise.resolve('') // poll external ip
22
repeat(10 * MINUTE, () => {
src/config.ts
+2
-2
@@ -1,11 +1,11 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import EventEmitter from 'events'
4
-import { argv, DAY, ORIGINAL_CWD, VERSION } from './const'
4
+import { argv, ORIGINAL_CWD, VERSION } from './const'
5
import { watchLoad } from './watchLoad'
6
import yaml from 'yaml'
7
import _ from 'lodash'
8
-import { debounceAsync, newObj, onOff, wait, with_ } from './misc'
8
+import { DAY, debounceAsync, newObj, onOff, wait, with_ } from './misc'
9
import { statSync } from 'fs'
10
import { join, resolve } from 'path'
11
import events from './events'
src/const.ts
-1
@@ -15,7 +15,6 @@ export const BUILD_TIMESTAMP = fs.statSync(PKG_PATH).mtime.toISOString()
15
const pkg = JSON.parse(fs.readFileSync(PKG_PATH,'utf8'))
16
export const VERSION = pkg.version
17
export const RUNNING_BETA = VERSION.includes('-')
18
-export const DAY = 86_400_000
18
19
export const API_VERSION = 8.3
20
export const COMPATIBLE_API_VERSION = 1 // while changes in the api are not breaking, this number stays the same, otherwise it is made equal to API_VERSION
src/cross.ts
+12
-2
@@ -5,6 +5,8 @@ import _ from 'lodash'
5
export const REPO_URL = 'https://github.com/rejetto/hfs/'
6
export const WIKI_URL = REPO_URL + 'wiki/'
7
export const MINUTE = 60_000
8
+export const HOUR = 60 * MINUTE
9
+export const DAY = 24 * HOUR
10
11
export type Dict<T=any> = Record<string, T>
12
export type Falsy = false | null | undefined | '' | 0
@@ -198,7 +200,7 @@ export function hasProp<T extends object>(obj: T, key: PropertyKey): key is keyo
200
return key in obj;
201
}
202
201
-export function throw_(err: any) {
203
+export function throw_(err: any): never {
204
throw err
205
}
206
@@ -220,4 +222,12 @@ export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Pro
222
export function repeat(every: number, cb: () => unknown) {
223
Promise.allSettled([cb()]).then(() =>
224
setTimeout(() => repeat(every, cb), every) )
223
-}
\ No newline at end of file
225
+}
226
+
227
+export function formatTimestamp(x: string) {
228
+ return x ? new Date(x).toLocaleString() : '-'
229
+}
230
+
231
+export function isPrimitive(x: unknown): x is boolean | string | number | undefined | null {
232
+ return !x || Object(x) !== x
233
+}
src/debounceAsync.ts
+15
-6
@@ -4,16 +4,19 @@
4
export default function debounceAsync<Cancelable extends boolean = false, A extends unknown[] = unknown[], R = unknown>(
5
callback: (...args: A) => Promise<R>,
6
wait: number=100,
7
- options: { leading?: boolean, maxWait?:number, cancelable?: Cancelable }={}
7
+ options: { leading?: boolean, maxWait?:number, retain?: number, retainFailure?: number, cancelable?: Cancelable }={}
8
) {
9
type MaybeUndefined<T> = Cancelable extends true ? undefined | T : T
10
type MaybeR = MaybeUndefined<R>
11
- const { leading=false, maxWait=Infinity, cancelable=false } = options
11
+ const { leading=false, maxWait=Infinity, cancelable=false, retain=0, retainFailure } = options
12
let started = 0 // latest callback invocation
13
let runningCallback: Promise<R> | undefined // latest callback invocation result
14
- let runningDebouncer: Promise<MaybeR> // latest wrapper invocation
14
+ let runningDebouncer: Promise<MaybeR | R> // latest wrapper invocation
15
let waitingSince = 0 // we are delaying invocation since
16
let whoIsWaiting: undefined | A // args object identifies the pending instance, and incidentally stores args
17
+ let last: typeof runningCallback
18
+ let lastFailed = false
19
+ let lastSince = 0
20
const interceptingWrapper = (...args: A) => runningDebouncer = debouncer(...args)
21
return Object.assign(interceptingWrapper, {
22
flush: () => runningCallback ?? exec(),
@@ -28,10 +31,13 @@ export default function debounceAsync<Cancelable extends boolean = false, A exte
31
async function debouncer(...args: A) {
32
if (runningCallback)
33
return runningCallback as MaybeR
34
+ const now = Date.now()
35
+ if (last && now - lastSince < (lastFailed ? retainFailure ?? retain : retain))
36
+ return await last
37
whoIsWaiting = args
32
- waitingSince ||= Date.now()
33
- const waitingCap = maxWait - (Date.now() - (waitingSince || started))
34
- const waitFor = Math.min(waitingCap, leading ? wait - (Date.now() - started) : wait)
38
+ waitingSince ||= now
39
+ const waitingCap = maxWait - (now - (waitingSince || started))
40
+ const waitFor = Math.min(waitingCap, leading ? wait - (now - started) : wait)
41
if (waitFor > 0)
42
await new Promise(resolve => setTimeout(resolve, waitFor))
43
if (!whoIsWaiting) // canceled
@@ -50,6 +56,9 @@ export default function debounceAsync<Cancelable extends boolean = false, A exte
56
return await runningCallback as MaybeUndefined<R> // await necessary to go-finally at the right time and even on exceptions
57
}
58
finally {
59
+ last = runningCallback
60
+ last!.then(() => lastFailed = false, () => lastFailed = true)
61
+ lastSince = Date.now()
62
whoIsWaiting = undefined
63
runningCallback = undefined
64
}
src/github.ts
+2
-2
@@ -1,7 +1,7 @@
1
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
import events from './events'
4
-import { httpString, httpStream, unzip, AsapStream } from './misc'
4
+import { DAY, httpString, httpStream, unzip, AsapStream } from './misc'
5
import {
6
DISABLING_POSTFIX, findPluginByRepo,
7
getAvailablePlugins,
@@ -12,7 +12,7 @@ import {
12
} from './plugins'
13
import { ApiError } from './apiMiddleware'
14
import _ from 'lodash'
15
-import { DAY, HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './const'
15
+import { HFS_REPO, HTTP_BAD_REQUEST, HTTP_CONFLICT, HTTP_NOT_ACCEPTABLE, HTTP_SERVER_ERROR } from './const'
16
import { rename, rm } from 'fs/promises'
17
import { join } from 'path'
18
import { readFileSync } from 'fs'
src/listen.ts
+8
-5
@@ -201,13 +201,15 @@ export async function getServerStatus() {
201
https: await serverStatus(httpsSrv, httpsPortCfg.get()),
202
}
203
204
- async function serverStatus(h: typeof httpSrv, configuredPort: number) {
205
- const busy = await h?.busy
204
+ async function serverStatus(srv: typeof httpSrv, configuredPort: number) {
205
+ const busy = await srv?.busy
206
await wait(0) // simple trick to wait for also .error to be updated. If this trickery becomes necessary elsewhere, then we should make also error a Promise.
207
return {
208
- ..._.pick(h, ['listening', 'error']),
208
+ ..._.pick(srv, ['listening', 'error']),
209
busy,
210
- port: (h?.address() as any)?.port as number || configuredPort,
210
+ port: (srv?.address() as any)?.port as number || configuredPort,
211
+ configuredPort,
212
+ srv,
213
}
214
}}
215
@@ -219,7 +221,8 @@ export async function getIps() {
221
&& v4first(onlyTruthy(nets.map(net => !net.internal && net.address)))[0] // for each interface we consider only 1 address
222
)).flat()
223
const e = await externalIp
222
- if (e) ips.unshift(e)
224
+ if (e && !ips.includes(e))
225
+ ips.unshift(e)
226
return v4first(ips)
227
.filter((x,i,a) => a.length > 1 || !x.startsWith('169.254')) // 169.254 = dhcp failure on the interface, but keep it if it's our only one
228
src/log.ts
+1
-2
@@ -6,11 +6,10 @@ import { defineConfig } from './config'
6
import { createWriteStream, renameSync } from 'fs'
7
import * as util from 'util'
8
import { stat } from 'fs/promises'
9
-import { DAY } from './const'
9
import _ from 'lodash'
10
import { createFileWithPath, prepareFolder } from './util-files'
11
import { getCurrentUsername } from './perm'
13
-import { makeNetMatcher, tryJson } from './misc'
12
+import { DAY, makeNetMatcher, tryJson } from './misc'
13
import events from './events'
14
15
class Logger {
src/middlewares.ts
+2
-3
@@ -3,14 +3,13 @@
3
import compress from 'koa-compress'
4
import Koa from 'koa'
5
import {
6
- ADMIN_URI, API_URI,
7
- BUILD_TIMESTAMP,
8
- DEV, DAY,
6
+ ADMIN_URI, API_URI, BUILD_TIMESTAMP, DEV,
7
HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_FOOL, HTTP_UNAUTHORIZED, HTTP_BAD_REQUEST,
8
} from './const'
9
import { FRONTEND_URI } from './const'
10
import { statusCodeForMissingPerm, nodeIsDirectory, urlToNode, vfs, walkNode, VfsNode, getNodeName } from './vfs'
11
import {
12
+ DAY,
13
asyncGeneratorToReadable,
14
dirTraversal,
15
filterMapGenerator,