better typing
Massimo Melina committed
Sep 2, 2023 at 23:42 UTC
f8fce64bc79313d3bab3a2314c6e3306355dbff1
3 files changed
+32
-22
src/api.net.ts
+9
-4
@@ -2,7 +2,7 @@
2
3
import { ApiError, ApiHandlers } from './apiMiddleware'
4
import { Client } from 'nat-upnp'
5
-import { HTTP_SERVICE_UNAVAILABLE, IS_MAC, IS_WINDOWS } from './const'
5
+import { HTTP_FAILED_DEPENDENCY, HTTP_SERVICE_UNAVAILABLE, IS_MAC, IS_WINDOWS } from './const'
6
import axios from 'axios'
7
import {parse} from 'node-html-parser'
8
import _ from 'lodash'
@@ -65,7 +65,9 @@ const apis: ApiHandlers = {
65
async map_port({ external }) {
66
const { gatewayIp, mapped, internalPort } = await getNatInfo()
67
if (!gatewayIp)
68
- throw new ApiError(HTTP_SERVICE_UNAVAILABLE, 'upnp failed')
68
+ return new ApiError(HTTP_SERVICE_UNAVAILABLE, 'upnp failed')
69
+ if (!internalPort)
70
+ return new ApiError(HTTP_FAILED_DEPENDENCY, 'no internal port')
71
const client = new Client()
72
if (mapped)
73
await client.removeMapping({ private: mapped.private.port, public: mapped.public.port, protocol: 'tcp' })
@@ -76,7 +78,10 @@ const apis: ApiHandlers = {
78
79
async check_server() {
80
const { publicIp, internalPort, externalPort } = await getNatInfo()
79
- if (!publicIp) return new ApiError(HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip')
81
+ if (!publicIp)
82
+ return new ApiError(HTTP_SERVICE_UNAVAILABLE, 'cannot detect public ip')
83
+ if (!internalPort)
84
+ return new ApiError(HTTP_FAILED_DEPENDENCY, 'no internal port')
85
const prjInfo = await getProjectInfo()
86
const port = externalPort || internalPort
87
console.log(`checking server ${publicIp}:${port}`)
@@ -95,7 +100,7 @@ const apis: ApiHandlers = {
100
const service = new URL(svc.url).hostname
101
console.log('trying service', service)
102
const api = (axios as any)[svc.method]
98
- const body = svc.body?.replace('$IP', publicIp).replace('$PORT', port) || ''
103
+ const body = svc.body?.replace('$IP', publicIp).replace('$PORT', String(port)) || ''
104
const res = await api(svc.url, body, {headers: svc.headers})
105
console.debug(service, 'responded')
106
const parsed = parse(res.data).querySelector(svc.selector)?.innerText
src/debounceAsync.ts
+21
-16
@@ -1,28 +1,33 @@
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
// like lodash.debounce, but also avoids async invocations to overlap
4
-export default function debounceAsync<CB extends (...args: any[]) => Promise<R>, R>(
5
- callback: CB,
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
- { leading=false, maxWait=Infinity }={}
7
+ options: { leading?: boolean, maxWait?: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
12
let started = 0 // latest callback invocation
13
let runningCallback: Promise<R> | undefined // latest callback invocation result
11
- let runningDebouncer: Promise<R | undefined> // latest wrapper invocation
14
+ let runningDebouncer: Promise<MaybeR> // latest wrapper invocation
15
let waitingSince = 0 // we are delaying invocation since
13
- let whoIsWaiting: undefined | any[] // args' array object identifies the pending instance, and incidentally stores args
14
- const interceptingWrapper = (...args:any[]) => runningDebouncer = debouncer.apply(null, args)
16
+ let whoIsWaiting: undefined | A // args object identifies the pending instance, and incidentally stores args
17
+ const interceptingWrapper = (...args: A) => runningDebouncer = debouncer(...args)
18
return Object.assign(interceptingWrapper, {
16
- cancel: () => {
17
- waitingSince = 0
18
- whoIsWaiting = undefined
19
- },
19
flush: () => runningCallback ?? exec(),
20
+ ...cancelable && {
21
+ cancel() {
22
+ waitingSince = 0
23
+ whoIsWaiting = undefined
24
+ }
25
+ }
26
})
27
23
- async function debouncer(...args:any[]) {
28
+ async function debouncer(...args: A) {
29
if (runningCallback)
25
- return runningCallback
30
+ return runningCallback as MaybeR
31
whoIsWaiting = args
32
waitingSince ||= Date.now()
33
const waitingCap = maxWait - (Date.now() - (waitingSince || started))
@@ -30,19 +35,19 @@ export default function debounceAsync<CB extends (...args: any[]) => Promise<R>,
35
if (waitFor > 0)
36
await new Promise(resolve => setTimeout(resolve, waitFor))
37
if (!whoIsWaiting) // canceled
33
- return void(waitingSince = 0)
38
+ return void(waitingSince = 0) as MaybeR
39
if (whoIsWaiting !== args) // another fresher call is waiting
40
return runningDebouncer
41
return exec()
42
}
43
44
async function exec() {
40
- if (!whoIsWaiting) return
45
+ if (!whoIsWaiting) return undefined as MaybeR
46
waitingSince = 0
47
started = Date.now()
48
try {
44
- runningCallback = callback.apply(null, whoIsWaiting)
45
- return await runningCallback // await necessary to go-finally at the right time and even on exceptions
49
+ runningCallback = callback(...whoIsWaiting)
50
+ return await runningCallback as MaybeUndefined<R> // await necessary to go-finally at the right time and even on exceptions
51
}
52
finally {
53
whoIsWaiting = undefined
src/listen.ts
+2
-2
@@ -200,13 +200,13 @@ export async function getServerStatus() {
200
https: await serverStatus(httpsSrv, httpsPortCfg.get()),
201
}
202
203
- async function serverStatus(h: typeof httpSrv, configuredPort?: number) {
203
+ async function serverStatus(h: typeof httpSrv, configuredPort: number) {
204
const busy = await h?.busy
205
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.
206
return {
207
..._.pick(h, ['listening', 'error']),
208
busy,
209
- port: (h?.address() as any)?.port || configuredPort,
209
+ port: (h?.address() as any)?.port as number || configuredPort,
210
}
211
}}
212