better code: client-server shared code

Massimo Melina committed Sep 6, 2023 at 12:47 UTC acce3ff90b81da954d1a882b0a6d6e30a02e597a
6 files changed +237 -317
frontend/src/i18n.ts
+4 -2
@@ -1,4 +1,6 @@
1 -import { findFirst, getHFS } from './misc'
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 { findDefined, getHFS } from './misc'
4 import { createElement as h, Fragment } from 'react'
5 import { proxy, useSnapshot } from 'valtio'
6
@@ -33,7 +35,7 @@ export function t(keyOrTpl: string | string[] | TemplateStringsArray, params?: a
35 let selectedLang = '' // keep track of where we find the translation
36 const { langs, embedded } = state
37 for (const key of keys) {
36 - found = findFirst(langs, lang => translations[selectedLang=lang]?.translate?.[key])
38 + found = findDefined(langs, lang => translations[selectedLang=lang]?.translate?.[key])
39 if (found) break
40 if (selectedLang && langs[0] !== embedded && !warns.has(key)) {
41 warns.add(key)
shared/index.ts
+1 -120
@@ -5,13 +5,7 @@ import { apiCall } from './api'
5 export * from './react'
6 export * from './dialogs'
7 export * from './srp'
8 -
9 -export const REPO_URL = 'https://github.com/rejetto/hfs/'
10 -export const WIKI_URL = REPO_URL + 'wiki/'
11 -
12 -export type Dict<T=any> = Record<string, T>
13 -export type Falsy = false | null | undefined | '' | 0
14 -type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
8 +export * from '../src/cross'
9
10 (window as any)._ = _
11
@@ -29,62 +23,6 @@ function getScriptAttr(k: string) {
23
24 export const urlParams = Object.fromEntries(new URLSearchParams(window.location.search).entries())
25
32 -const MULTIPLIERS = ['', 'K', 'M', 'G', 'T']
33 -export function formatBytes(n: number, { post='B', k=1024, digits=NaN }={}) {
34 - if (isNaN(Number(n)) || n < 0)
35 - return ''
36 - const i = n && Math.floor(Math.log2(n) / Math.log2(k))
37 - n /= k ** i
38 - const nAsString = i && !isNaN(digits) ? n.toFixed(digits)
39 - : _.round(n, isNaN(digits) ? (n >= 100 ? 0 : 1) : digits)
40 - return nAsString + ' ' + (MULTIPLIERS[i]||'') + post
41 -} // formatBytes
42 -
43 -export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
44 - return v ? pre+v+post : ''
45 -}
46 -
47 -export function wait<T=undefined>(ms: number, val?: T): Promise<T> {
48 - return new Promise(res=> setTimeout(res,ms,val))
49 -}
50 -
51 -export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:Truthy<S[keyof S]>, key:keyof S)=>VR) {
52 - return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k as keyof S)])) as { [K in keyof S]:VR }
53 -}
54 -
55 -export function enforceFinal(sub:string, s:string) {
56 - return !s || s.endsWith(sub) ? s : s+sub
57 -}
58 -
59 -export function truthy<T>(value: T): value is Truthy<T> {
60 - return Boolean(value)
61 -}
62 -
63 -export function onlyTruthy<T>(arr: T[]) {
64 - return arr.filter(truthy)
65 -}
66 -
67 -export function setHidden(dest: object, src:object) {
68 - return Object.defineProperties(dest, objSameKeys(src as any, value => ({
69 - enumerable: false,
70 - writable: true,
71 - value,
72 - })))
73 -}
74 -
75 -export function try_(cb: () => any, onException?: (e:any) => any) {
76 - try {
77 - return cb()
78 - }
79 - catch(e) {
80 - return onException?.(e)
81 - }
82 -}
83 -
84 -export function with_<T,RT>(par:T, cb: (par:T) => RT) {
85 - return cb(par)
86 -}
87 -
26 export function domOn<K extends keyof WindowEventMap>(eventName: K, cb: (ev: WindowEventMap[K]) => void, { target=window }={}) {
27 target.addEventListener(eventName, cb)
28 return () => target.removeEventListener(eventName, cb)
@@ -96,14 +34,6 @@ export function restartAnimation(e: HTMLElement, animation: string) {
34 e.style.animation = animation
35 }
36
99 -export function findFirst<I, O>(a: I[] | Record<string, I>, cb:(v:I, k: string | number)=>O): any {
100 - if (a) for (const k in a) {
101 - const ret = cb((a as any)[k] as I, k)
102 - if (ret !== undefined)
103 - return ret
104 - }
105 -}
106 -
37 export function selectFiles(cb: (list: FileList | null)=>void, { accept='', multiple=true, folder=false }={}) {
38 const el = Object.assign(document.createElement('input'), {
39 type: 'file',
@@ -133,27 +63,6 @@ export function readFile(f: File | Blob): Promise<string | undefined> {
63 })
64 }
65
136 -export function formatPerc(p: number) {
137 - return (p*100).toFixed(1) + '%'
138 -}
139 -
140 -export function wantArray<T>(x?: void | T | T[]) {
141 - return x == null ? [] : Array.isArray(x) ? x : [x]
142 -}
143 -
144 -export function _log(...args: any[]) {
145 - console.log('**', ...args)
146 - return args[args.length-1]
147 -}
148 -
149 -type PendingPromise<T> = Promise<T> & { resolve: (value: T) => void, reject: (reason?: any) => void }
150 -export function pendingPromise<T>() {
151 - let takeOut
152 - const ret = new Promise<T>((resolve, reject) =>
153 - takeOut = { resolve, reject })
154 - return Object.assign(ret, takeOut) as PendingPromise<T>
155 -}
156 -
66 export function isMobile() {
67 return window.innerWidth < 800
68 }
@@ -166,10 +75,6 @@ export function getPrefixUrl() {
75 return getHFS().prefixUrl || ''
76 }
77
169 -export function basename(path: string) {
170 - return path.slice(path.lastIndexOf('/') + 1 || path.lastIndexOf('\\') + 1)
171 -}
172 -
78 export function makeSessionRefresher(state: any) {
79 return function sessionRefresher(response: any) {
80 if (!response) return
@@ -183,27 +88,3 @@ export function makeSessionRefresher(state: any) {
88 setTimeout(() => apiCall('refresh_session').then(sessionRefresher), t)
89 }
90 }
186 -
187 -export function tryJson(s?: string) {
188 - try { return s && JSON.parse(s) }
189 - catch {}
190 -}
191 -
192 -export function swap<T>(obj: T, k1: keyof T, k2: keyof T) {
193 - const temp = obj[k1]
194 - obj[k1] = obj[k2]
195 - obj[k2] = temp
196 - return obj
197 -}
198 -
199 -export function isOrderedEqual(a: any, b: any): boolean {
200 - return _.isEqualWith(a, b, (a1, b1) => {
201 - if (!_.isPlainObject(a1) || !_.isPlainObject(b1)) return
202 - const ka = Object.keys(a1)
203 - const kb = Object.keys(b1)
204 - return ka.length === kb.length && ka.every((ka1, i) => {
205 - const kb1 = kb[i]
206 - return ka1 === kb1 && isOrderedEqual(a1[ka1], b1[kb1])
207 - })
208 - })
209 -}
\ No newline at end of file
src/cross.ts new
+218
@@ -0,0 +1,218 @@
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 +// all content here is shared between client and server
3 +import _ from 'lodash'
4 +
5 +export const REPO_URL = 'https://github.com/rejetto/hfs/'
6 +export const WIKI_URL = REPO_URL + 'wiki/'
7 +
8 +export type Dict<T=any> = Record<string, T>
9 +export type Falsy = false | null | undefined | '' | 0
10 +type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
11 +export type Callback<IN=void, OUT=void> = (x:IN) => OUT
12 +export type Promisable<T> = T | Promise<T>
13 +
14 +const MULTIPLIERS = ['', 'K', 'M', 'G', 'T']
15 +export function formatBytes(n: number, { post='B', k=1024, digits=NaN }={}) {
16 + if (isNaN(Number(n)) || n < 0)
17 + return ''
18 + const i = n && Math.floor(Math.log2(n) / Math.log2(k))
19 + n /= k ** i
20 + const nAsString = i && !isNaN(digits) ? n.toFixed(digits)
21 + : _.round(n, isNaN(digits) ? (n >= 100 ? 0 : 1) : digits)
22 + return nAsString + ' ' + (MULTIPLIERS[i]||'') + post
23 +} // formatBytes
24 +
25 +export function prefix(pre:string, v:string|number|undefined|null|false, post:string='') {
26 + return v ? pre+v+post : ''
27 +}
28 +
29 +export function wait<T=undefined>(ms: number, val?: T): Promise<T | undefined> {
30 + return new Promise(res=> setTimeout(res,ms,val))
31 +}
32 +
33 +export function objSameKeys<S extends object,VR=any>(src: S, newValue:(value:Truthy<S[keyof S]>, key:keyof S)=>VR) {
34 + return Object.fromEntries(Object.entries(src).map(([k,v]) => [k, newValue(v,k as keyof S)])) as { [K in keyof S]:VR }
35 +}
36 +
37 +export function enforceFinal(sub:string, s:string) {
38 + return !s || s.endsWith(sub) ? s : s+sub
39 +}
40 +
41 +export function truthy<T>(value: T): value is Truthy<T> {
42 + return Boolean(value)
43 +}
44 +
45 +export function onlyTruthy<T>(arr: T[]) {
46 + return arr.filter(truthy)
47 +}
48 +
49 +export function setHidden<T, ADD>(dest: T, src: ADD) {
50 + return Object.defineProperties(dest, newObj(src as any, value => ({
51 + enumerable: false,
52 + writable: true,
53 + value,
54 + }))) as T & ADD
55 +}
56 +
57 +export function try_(cb: () => any, onException?: (e:any) => any) {
58 + try {
59 + return cb()
60 + }
61 + catch(e) {
62 + return onException?.(e)
63 + }
64 +}
65 +
66 +export function with_<T,RT>(par:T, cb: (par:T) => RT) {
67 + return cb(par)
68 +}
69 +
70 +export function formatPerc(p: number) {
71 + return (p*100).toFixed(1) + '%'
72 +}
73 +
74 +export function wantArray<T>(x?: void | T | T[]) {
75 + return x == null ? [] : Array.isArray(x) ? x : [x]
76 +}
77 +
78 +export function _log(...args: any[]) {
79 + console.log('**', ...args)
80 + return args[args.length-1]
81 +}
82 +
83 +export type PendingPromise<T=unknown> = Promise<T> & { resolve: (value?: T) => void, reject: (reason?: any) => void }
84 +export function pendingPromise<T>() {
85 + let takeOut
86 + const ret = new Promise<T>((resolve, reject) =>
87 + takeOut = { resolve, reject })
88 + return Object.assign(ret, takeOut) as PendingPromise<T>
89 +}
90 +
91 +export function basename(path: string) {
92 + return path.slice(path.lastIndexOf('/') + 1 || path.lastIndexOf('\\') + 1)
93 +}
94 +
95 +export function tryJson(s?: string) {
96 + try { return s && JSON.parse(s) }
97 + catch {}
98 +}
99 +
100 +export function swap<T>(obj: T, k1: keyof T, k2: keyof T) {
101 + const temp = obj[k1]
102 + obj[k1] = obj[k2]
103 + obj[k2] = temp
104 + return obj
105 +}
106 +
107 +export function isOrderedEqual(a: any, b: any): boolean {
108 + return _.isEqualWith(a, b, (a1, b1) => {
109 + if (!_.isPlainObject(a1) || !_.isPlainObject(b1)) return
110 + const ka = Object.keys(a1)
111 + const kb = Object.keys(b1)
112 + return ka.length === kb.length && ka.every((ka1, i) => {
113 + const kb1 = kb[i]
114 + return ka1 === kb1 && isOrderedEqual(a1[ka1], b1[kb1])
115 + })
116 + })
117 +}
118 +
119 +export function findDefined<I, O>(a: I[] | Record<string, I>, cb:(v:I, k: string | number)=>O): any {
120 + if (a) for (const k in a) {
121 + const ret = cb((a as any)[k] as I, k)
122 + if (ret !== undefined)
123 + return ret
124 + }
125 +}
126 +
127 +export function removeStarting(sub: string, s: string) {
128 + return s.startsWith(sub) ? s.slice(sub.length) : s
129 +}
130 +
131 +export function newObj<S extends (object | undefined | null),VR=any>(
132 + src: S,
133 + returnNewValue: (value:Truthy<S[keyof S]>, key: Exclude<keyof S, symbol>, setK:(newK?: string)=>true, depth: number) => any,
134 + recur: boolean | number=false
135 +) {
136 + if (!src)
137 + return {}
138 + const pairs = Object.entries(src).map( ([k,v]) => {
139 + if (typeof k === 'symbol') return
140 + let _k: undefined | typeof k = k
141 + const curDepth = typeof recur === 'number' ? recur : 0
142 + let newV = returnNewValue(v, k as Exclude<keyof S, symbol>, (newK) => {
143 + _k = newK
144 + return true // for convenient expression concatenation
145 + }, curDepth)
146 + if ((recur !== false || returnNewValue.length === 4) // if callback is using depth parameter, then it wants recursion
147 + && _.isPlainObject(newV)) // is it recurrable?
148 + newV = newObj(newV, returnNewValue, curDepth + 1)
149 + return _k !== undefined && [_k, newV]
150 + })
151 + return Object.fromEntries(onlyTruthy(pairs)) as S extends undefined | null ? S : { [K in keyof S]:VR }
152 +}
153 +
154 +export async function waitFor<T>(cb: ()=> T, { interval=200, timeout=Infinity }={}) {
155 + const started = Date.now()
156 + while (1) {
157 + const res = await cb()
158 + if (res)
159 + return res
160 + if (Date.now() - started >= timeout)
161 + return
162 + await wait(interval)
163 + }
164 +}
165 +
166 +export function getOrSet<T>(o: Record<string,T>, k:string, creator:()=>T): T {
167 + return k in o ? o[k]!
168 + : (o[k] = creator())
169 +}
170 +
171 +// 10 chars is 51+bits, 8 is 41+bits
172 +export function randomId(len = 10): string {
173 + if (len > 10)
174 + return randomId(10) + randomId(len - 10)
175 + return Math.random()
176 + .toString(36)
177 + .substring(2, 2+len)
178 + .replace(/l/g, 'L'); // avoid confusion reading l1
179 +}
180 +
181 +export function objRenameKey(o: Dict | undefined, from: string, to: string) {
182 + if (!o || !o.hasOwnProperty(from) || from === to) return
183 + o[to] = o[from]
184 + delete o[from]
185 + return true
186 +}
187 +
188 +export function typedKeys<T extends {}>(o: T) {
189 + return Object.keys(o) as (keyof T)[]
190 +}
191 +
192 +export function typedEntries<T extends {}>(o: T): [keyof T, T[keyof T]][] {
193 + return Object.entries(o) as [keyof T, T[keyof T]][];
194 +}
195 +
196 +export function hasProp<T extends object>(obj: T, key: PropertyKey): key is keyof T {
197 + return key in obj;
198 +}
199 +
200 +export function throw_(err: any) {
201 + throw err
202 +}
203 +
204 +export async function* filterMapGenerator<IN,OUT>(generator: AsyncIterableIterator<IN>, filterMap: (el: IN) => Promise<OUT>) {
205 + for await (const x of generator) {
206 + const res:OUT = await filterMap(x)
207 + if (res !== undefined)
208 + yield res as Exclude<OUT,undefined>
209 + }
210 +}
211 +
212 +export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Promise<T[]> {
213 + const ret: T[] = []
214 + for await(const x of generator)
215 + ret.push(x)
216 + return ret
217 +}
218 +
src/misc.ts
+11 -154
@@ -7,96 +7,14 @@ import Koa from 'koa'
7 import { Connection } from './connections'
8 import assert from 'assert'
9 export * from './util-http'
10 -export * from './util-generators'
10 export * from './util-files'
12 -import debounceAsync from './debounceAsync'
11 +export * from './cross'
12 import { Readable } from 'stream'
13 import { matcher } from 'micromatch'
14 import cidr from 'cidr-tools'
15 +import debounceAsync from './debounceAsync'
16 export { debounceAsync }
17
18 -export type Callback<IN=void, OUT=void> = (x:IN) => OUT
19 -export type Dict<T = any> = Record<string, T>
20 -export type Promisable<T> = T | Promise<T>
21 -
22 -export function enforceFinal(sub:string, s:string) {
23 - return s.endsWith(sub) ? s : s+sub
24 -}
25 -
26 -export function removeStarting(sub: string, s: string) {
27 - return s.startsWith(sub) ? s.slice(sub.length) : s
28 -}
29 -
30 -export function prefix(pre:string, v:string|number|undefined, post:string='') {
31 - return v ? pre+v+post : ''
32 -}
33 -
34 -export function setHidden<T, ADD>(dest: T, src: ADD) {
35 - return Object.defineProperties(dest, newObj(src as any, value => ({
36 - enumerable: false,
37 - writable: true,
38 - value,
39 - }))) as T & ADD
40 -}
41 -
42 -export function newObj<S extends (object | undefined | null),VR=any>(
43 - src: S,
44 - returnNewValue: (value:Truthy<S[keyof S]>, key: Exclude<keyof S, symbol>, setK:(newK?: string)=>true, depth: number) => any,
45 - recur: boolean | number=false
46 -) {
47 - if (!src)
48 - return {}
49 - const pairs = Object.entries(src).map( ([k,v]) => {
50 - if (typeof k === 'symbol') return
51 - let _k: undefined | typeof k = k
52 - const curDepth = typeof recur === 'number' ? recur : 0
53 - let newV = returnNewValue(v, k as Exclude<keyof S, symbol>, (newK) => {
54 - _k = newK
55 - return true // for convenient expression concatenation
56 - }, curDepth)
57 - if ((recur !== false || returnNewValue.length === 4) // if callback is using depth parameter, then it wants recursion
58 - && _.isPlainObject(newV)) // is it recurrable?
59 - newV = newObj(newV, returnNewValue, curDepth + 1)
60 - return _k !== undefined && [_k, newV]
61 - })
62 - return Object.fromEntries(onlyTruthy(pairs)) as S extends undefined | null ? S : { [K in keyof S]:VR }
63 -}
64 -
65 -export function wait(ms: number) {
66 - return new Promise(res=> setTimeout(res,ms))
67 -}
68 -
69 -export async function waitFor<T>(cb: ()=> T, { interval=200, timeout=Infinity }={}) {
70 - const started = Date.now()
71 - while (1) {
72 - const res = await cb()
73 - if (res)
74 - return res
75 - if (Date.now() - started >= timeout)
76 - return
77 - await wait(interval)
78 - }
79 -}
80 -
81 -export function wantArray<T>(x?: void | T | T[]) {
82 - return x == null ? [] : Array.isArray(x) ? x : [x]
83 -}
84 -
85 -export function getOrSet<T>(o: Record<string,T>, k:string, creator:()=>T): T {
86 - return k in o ? o[k]!
87 - : (o[k] = creator())
88 -}
89 -
90 -// 10 chars is 51+bits, 8 is 41+bits
91 -export function randomId(len = 10): string {
92 - if (len > 10)
93 - return randomId(10) + randomId(len - 10)
94 - return Math.random()
95 - .toString(36)
96 - .substring(2, 2+len)
97 - .replace(/l/g, 'L'); // avoid confusion reading l1
98 -}
99 -
18 type ProcessExitHandler = (signal:string) => any
19 const cbs = new Set<ProcessExitHandler>()
20 export function onProcessExit(cb: ProcessExitHandler) {
@@ -123,24 +41,6 @@ export function pattern2filter(pattern: string){
41 !s || !pattern || re.test(basename(s))
42 }
43
126 -type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T
127 -
128 -export function truthy<T>(value: T): value is Truthy<T> {
129 - return Boolean(value)
130 -}
131 -
132 -export function onlyTruthy<T>(arr: T[]) {
133 - return arr.filter(truthy)
134 -}
135 -
136 -export type PendingPromise<T=unknown> = Promise<T> & { resolve: (value?: T) => void, reject: (reason?: any) => void }
137 -export function pendingPromise<T>() {
138 - let takeOut
139 - const ret = new Promise<T>((resolve, reject) =>
140 - takeOut = { resolve, reject })
141 - return Object.assign(ret, takeOut) as PendingPromise<T>
142 -}
143 -
44 // install multiple handlers and returns a handy 'uninstall' function which requires no parameter. Pass a map {event:handler}
45 export function onOff(em: EventEmitter, events: { [eventName:string]: (...args: any[]) => void }) {
46 events = { ...events } // avoid later modifications, as we need this later for uninstallation
@@ -154,29 +54,6 @@ export function onOff(em: EventEmitter, events: { [eventName:string]: (...args:
54 }
55 }
56
157 -export function objRenameKey(o: Dict | undefined, from: string, to: string) {
158 - if (!o || !o.hasOwnProperty(from) || from === to) return
159 - o[to] = o[from]
160 - delete o[from]
161 - return true
162 -}
163 -
164 -export function typedKeys<T extends {}>(o: T) {
165 - return Object.keys(o) as (keyof T)[]
166 -}
167 -
168 -export function typedEntries<T extends {}>(o: T): [keyof T, T[keyof T]][] {
169 - return Object.entries(o) as [keyof T, T[keyof T]][];
170 -}
171 -
172 -export function hasProp<T extends object>(obj: T, key: PropertyKey): key is keyof T {
173 - return key in obj;
174 -}
175 -
176 -export function with_<T,RT>(par:T, cb: (par:T) => RT) {
177 - return cb(par)
178 -}
179 -
57 export function isLocalHost(c: Connection | Koa.Context) {
58 const ip = c.socket.remoteAddress // don't use Context.ip as it is subject to proxied ips, and that's no use for localhost detection
59 return ip && (ip === '::1' || ip.endsWith('127.0.0.1'))
@@ -195,11 +72,6 @@ export function makeNetMatcher(mask: string, emptyMaskReturns=false) {
72 neg !== all.some(x => cidr.contains(x, ip))
73 }
74
198 -export function netmaskToCIDR(netmask: string) {
199 - return netmask.split('.').map(x => Number(x).toString(2)) // to binary
200 - .join('').split('1').length - 1 // Count '1' bits
201 -}
202 -
75 export function makeMatcher(mask: string, emptyMaskReturns=false) {
76 return mask ? matcher(mask.replace(/^(!)?/, '$1(') + ')') // adding () will allow us to use the pipe at root level
77 : () => emptyMaskReturns
@@ -217,11 +89,6 @@ export function same(a: any, b: any) {
89 catch { return false }
90 }
91
220 -export function tryJson(s?: string) {
221 - try { return s ? JSON.parse(s) : undefined }
222 - catch {}
223 -}
224 -
92 export async function stream2string(stream: Readable): Promise<string> {
93 return new Promise((resolve, reject) => {
94 let data = ''
@@ -239,23 +106,13 @@ export async function stream2string(stream: Readable): Promise<string> {
106 })
107 }
108
242 -export function try_(cb: () => any, onException?: (e:any) => any) {
243 - try {
244 - return cb()
245 - }
246 - catch(e) {
247 - return onException?.(e)
248 - }
249 -}
250 -
251 -export function throw_(err: any) {
252 - throw err
253 -}
254 -
255 -export function findFirst<I, O>(a: I[] | Record<string, I>, cb:(v:I, k: string | number)=>O): any {
256 - if (a) for (const k in a) {
257 - const ret = cb((a as any)[k] as I, k)
258 - if (ret !== undefined)
259 - return ret
260 - }
109 +export function asyncGeneratorToReadable<T>(generator: AsyncIterable<T>) {
110 + const iterator = generator[Symbol.asyncIterator]()
111 + return new Readable({
112 + objectMode: true,
113 + read() {
114 + iterator.next().then(it =>
115 + this.push(it.done ? null : it.value))
116 + }
117 + })
118 }
src/util-generators.ts deleted
-31
@@ -1,31 +0,0 @@
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 -// callback can return undefined to skip element
4 -import { Readable } from 'stream'
5 -
6 -export async function* filterMapGenerator<IN,OUT>(generator: AsyncIterableIterator<IN>, filterMap: (el: IN) => Promise<OUT>) {
7 - for await (const x of generator) {
8 - const res:OUT = await filterMap(x)
9 - if (res !== undefined)
10 - yield res as Exclude<OUT,undefined>
11 - }
12 -}
13 -
14 -export async function asyncGeneratorToArray<T>(generator: AsyncIterable<T>): Promise<T[]> {
15 - const ret: T[] = []
16 - for await(const x of generator)
17 - ret.push(x)
18 - return ret
19 -}
20 -
21 -export function asyncGeneratorToReadable<T>(generator: AsyncIterable<T>) {
22 - const iterator = generator[Symbol.asyncIterator]()
23 - return new Readable({
24 - objectMode: true,
25 - read() {
26 - iterator.next().then(it =>
27 - this.push(it.done ? null : it.value))
28 - }
29 - })
30 -}
31 -
tests/test.ts
+3 -10
@@ -5,6 +5,7 @@ import { srpSequence } from '@hfs/shared/srp'
5 import { createReadStream, rmSync } from 'fs'
6 import { join } from 'path'
7 import _ from 'lodash'
8 +import { findDefined } from '../src/cross'
9 /*
10 import { PORT, srv } from '../src'
11
@@ -165,10 +166,10 @@ function req(methodUrl: string, test:Tester, requestOptions: AxiosRequestConfig<
166 || re && !(typeof data === 'string' && re.test(data)) && 'expected content '+String(re)+' got '+(data || '-empty-')
167 || inList && !inList.every(x => isInList(data, x)) && 'expected in list '+inList
168 || outList && !outList.every(x => !isInList(data, x)) && 'expected not in list '+outList
168 - || permInList && findFirst(permInList, (v, k) => {
169 + || permInList && findDefined(permInList, (v, k) => {
170 const got = _.find(data.list, { n: k })?.p
171 const negate = v[0] === '!'
171 - return findFirst(v.slice(negate ? 1 : 0).split(''), char =>
172 + return findDefined(v.slice(negate ? 1 : 0).split(''), char =>
173 got?.includes(char) === negate ? `expected perm ${v} on ${k}, got ${got}` : undefined)
174 })
175 || test.empty && data && 'expected empty body'
@@ -195,12 +196,4 @@ function reqList(uri:string, tester:Tester, params?: object) {
196
197 function isInList(res:any, name:string) {
198 return Array.isArray(res?.list) && Boolean((res.list as any[]).find(x => x.n===name))
198 -}
199 -
200 -export function findFirst<I, O>(a: I[] | Record<string, I>, cb:(v:I, k: string | number)=>O): any {
201 - if (a) for (const k in a) {
202 - const ret = cb((a as any)[k] as I, k)
203 - if (ret !== undefined)
204 - return ret
205 - }
199 }
\ No newline at end of file