Some utils and remix that I repeated in many projects.
This package includes some light-weight alternatives to packages like:
our | is alternative to / remix of |
---|---|
elt / clsx | clsx, classnames, h, hyperscript |
maybeAsync / makePromise / PromiseEx | imperative-promise, bluebird |
stringHash | cyrb53, murmurhash ... |
<some lodash-like functions> | lodash |
There are also some interesting original utils like shallowEqual / newFunction / toArray / getVariableName etc. Feel free to explore!
All modules are shipped as ES modules and tree-shakable.
-
via package manager
npm install yon-utils
-
via import within
<script type="module">
import { elt } from "https://unpkg.com/yon-utils"
module | methods |
---|---|
dom | writeClipboard / readClipboard / clsx / elt / modKey / startMouseMove |
flow | delay / debouncePromise / fnQueue / makeAsyncIterator / makeEffect / maybeAsync / makePromise / PromiseEx / PromisePendingError / timing / withDefer / withAsyncDefer |
manager | ModuleLoader / CircularDependencyError / getSearchMatcher |
type | is / shallowEqual / newFunction / noop / approx / isInsideRect / isRectEqual / getRectIntersection / toArray / find / reduce / head / contains / forEach / stringHash / getVariableName / bracket / isNil / isObject / isThenable |
-
text:
string
-
Returns:
Promise<void>
write text to clipboard, with support for insecure context and legacy browser!
note: if you are in HTTPS and modern browser, you can directly use navigator.clipboard.writeText()
instead.
-
timeout?:
number
β default 1500 -
Returns:
Promise<string>
read clipboard text.
if user rejects or hesitates about the permission for too long, this will throw an Error.
-
args:
any[]
-
Returns:
string
construct className strings conditionally.
can be an alternative to classnames()
. modified from lukeed/clsx. to integrate with Tailwind VSCode, read this
-
tagName:
string
β for example"div"
or"button.my-btn"
-
attrs:
any
β attribute values to be set. beware:-
onClick
and afunction
value, will be handled byaddEventListener()
-
!onClick
oronClick.capture
will make it capture -
style
value could be a string or object -
class
value could be a string, object or array, and will be process byclsx()
-
className
is alias ofclass
-
-
children:
any[]
β can be strings, numbers, nodes. other types or nils will be omitted. -
Returns:
HTMLElement
Make document.createElement
easier
var button = elt(
'button.myButton', // tagName, optionally support .className and #id
{
title: "a magic button",
class: { isPrimary: xxx.xxx }, // className will be processed by clsx
onclick: () => alert('hi')
},
'Click Me!'
)
This function can be used as a jsxFactory, aka JSX pragma.
You can add /** @jsx elt */
into your code, then TypeScript / Babel will use elt
to process JSX expressions:
/** @jsx elt */
var button = <button class="myButton" onclick={...}>Click Me</button>
-
ev:
KeyboardEventLike
-
ctrlKey?:
boolean
-
metaKey?:
boolean
-
shiftKey?:
boolean
-
altKey?:
boolean
-
-
Returns:
number
get Modifier Key status from a Event
- use
modKey.Mod
to indicate if the key isβ
(Cmd) on Mac, orCtrl
on Windows/Linux - use
|
(or operator) to combine modifier keys. see example below.
if (modKey(ev) === (modKey.Mod | modKey.Shift) && ev.code === 'KeyW') {
// Ctrl/Cmd + Shift + W, depends on the OS
}
-
__0:
MouseMoveInitOptions
-
initialEvent:
MouseEvent | PointerEvent
-
onMove?:
(data: MouseMoveInfo) => void
-
onEnd?:
(data: MouseMoveInfo) => void
-
-
Returns:
Promise<MouseMoveInfo>
β - the final position when user releases button
use this in mousedown
or pointerdown
and it will keep tracking the cursor's movement, calling your onMove(...)
, until user releases the button.
(not support β touchstart
-- use β
pointerdown
instead)
button.addEventListener('pointerdown', event => {
event.preventDefault();
startMouseMove({
initialEvent: event,
onMove({ deltaX, deltaY }) { ... },
onEnd({ deltaX, deltaY }) { ... },
});
});
-
milliseconds:
number
-
Returns:
Promise<void>
-
fn:
() => Promise<T>
β The function to be debounced. -
Returns:
() => Promise<T>
β The debounced function.
Creates a debounced version of a function that returns a promise.
The returned function will ensure that only one Promise is created and executed at a time, even if the debounced function is called multiple times before last Promise gets finished.
All suppressed calls will get the last started Promise.
-
async?:
boolean
β if true, all queued functions are treated as async, and we return a Promise in the end. -
reversed?:
boolean
β if true, the order of execution is reversed (FILO, like a stack) -
error?:
"abort" | "throwLastError" | "ignore"
β if met error, shall we 'abort' immediately, or 'throwLastError', or 'ignore' all errors -
Returns:
FnQueue<ARGS, void>
-
tap:
Tap<ARGS> & { silent: Tap<ARGS>; }
β add functions to queue. see example. use tap.silent(fns) to ignore errors -
tapSilent:
Tap<ARGS>
β add functions to queue, but silently ignore their errors (identical to tap.silent) -
call:
(...args: ARGS) => RET
β clear the queue, execute functions -
queue:
{ silent?: boolean | undefined; fn: Fn<any, ARGS>; }[]
β the queued functions
-
With fnQueue
, you can implement a simple disposer to avoid resource leaking.
Order of execution: defaults to FIFO (first in, last run); set 1st argument to true
to reverse the order (FILO)
Exceptions: queued functions shall NOT throw errors, otherwise successive calls will be aborted.
const dispose = fnQueue();
try {
const srcFile = await openFile(path1);
dispose.tap(() => srcFile.close());
const dstFile = await openFile(path2);
opDispose.tap(() => dstFile.close());
await copyData(srcFile, dstFile);
} finally {
// first call:
dispose(); // close handles
// second call:
dispose(); // nothing happens -- the queue is emptied
}
- Returns:
{ write(value: T): void; end(error?: any): void; } & AsyncIterableIterator<T>
Help you convert a callback-style stream into an async iterator. Also works on "observable" value like RxJS.
You can think of this as a simplified new Readable({ ... })
without headache.
const iterator = makeAsyncIterator();
socket.on('data', value => iterator.write(value));
socket.on('end', () => iterator.end());
socket.on('error', (err) => iterator.end(err));
for await (const line of iterator) {
console.log(line);
}
-
fn:
(input: T, previous: T | undefined) => void | (() => void)
-
isEqual?:
(x: T, y: T) => boolean
-
Returns:
{ (input: T): void; cleanup(): void; readonly value: T | undefined; }
-
cleanup:
() => void
β invoke last cleanup function, and resetvalue
to undefined -
value?:
NonNullable<T>
β get last received value, orundefined
if effect was clean up
-
Wrap fn
and create an unary function. The actual fn()
executes only when the argument changes.
Meanwhile, your fn
may return a cleanup function, which will be invoked before new fn()
calls
-- just like React's useEffect
The new unary function also provide cleanup()
method to forcedly do the cleanup, which will also clean the memory of last input.
const sayHi = makeEffect((name) => {
console.log(`Hello, ${name}`);
return () => {
console.log(`Goodbye, ${name}`);
}
});
sayHi('Alice'); // output: Hello, Alice
sayHi('Alice'); // no output
sayHi('Bob'); // output: Goodbye, Alice Hello, Bob
sayHi.cleanup(); // output: Goodbye, Bob
sayHi.cleanup(); // no output
-
input:
T | Promise<T> | (() => T | Promise<T>)
β your sync/async function to run, or just a value -
Returns:
PromiseEx<Awaited<T>>
β a crafted Promise that exposes{ status, value, reason }
, whosestatus
could be"pending" | "fulfilled" | "rejected"
-
status:
"pending" | "fulfilled" | "rejected"
-
reason:
any
β if rejected, get the reason. -
result?:
NonNullable<T>
β get result, or nothing if not fulfilled.note: you might need
.value
which follows fail-fast mentality -
loading:
boolean
β equivalent to.status === "pending"
-
value?:
NonNullable<T>
β fail-fast mentality, safely get the result.- if pending, throw
new PromisePendingError(this)
- if rejected, throw
.reason
- if fulfilled, get
.result
- if pending, throw
-
wait:
(timeout: number) => Promise<T>
β wait for resolved / rejected.optionally can set a timeout in milliseconds. if timeout, a
PromisePendingError
will be thrown -
thenImmediately:
<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | Nil, onrejected?: Nil | ((reason: any) => TResult2 | PromiseLike<...>)) => PromiseEx<...>
β Likethen()
but immediately invoke callbacks, if this PromiseEx is already resolved / rejected.
-
Run the function, return a crafted Promise that exposes status
, value
and reason
If input
is sync function, its result will be stored in promise.value
and promise.status
will immediately be set as "fulfilled"
Useful when you are not sure whether fn
is async or not.
- Returns:
ImperativePromiseEx<T>
Create an imperative Promise.
Returns a Promise with these 2 methods exposed, so you can control its behavior:
.resolve(result)
.reject(error)
Besides, the returned Promise will expose these useful properties so you can get its status easily:
-
.wait([timeout])
β wait for result, if timeout set and exceeded, aPromisePendingError
will be thrown -
.status
β could be"pending" | "fulfilled" | "rejected"
-
.result
and.reason
-
.value
β fail-safe get result (or cause an Error from rejection, or cause aPromisePendingError
if still pending)
const handler = makePromise();
doSomeRequest(..., result => handler.resolve(result));
// wait with timeout
const result = await handler.wait(1000);
// or just await
const result = await handler;
-
executor:
(resolve: (value: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void
a crafted Promise that exposes { status, value, reason }
Note: please use maybeAsync()
or PromiseEx.resolve()
to create a PromiseEx
π show members of PromiseEx
Β»
- Type:
"pending" | "fulfilled" | "rejected"
- Type:
any
if rejected, get the reason.
- Type:
T | undefined
get result, or nothing if not fulfilled.
note: you might need .value
which follows fail-fast mentality
- Type:
boolean
equivalent to .status === "pending"
- Type:
T | undefined
fail-fast mentality, safely get the result.
- if pending, throw
new PromisePendingError(this)
- if rejected, throw
.reason
- if fulfilled, get
.result
-
timeout:
number
-
Returns:
Promise<T>
wait for resolved / rejected.
optionally can set a timeout in milliseconds. if timeout, a PromisePendingError
will be thrown
-
onfulfilled?:
(value: T) => TResult1 | PromiseLike<TResult1>
-
onrejected?:
(reason: any) => TResult2 | PromiseLike<TResult2>
-
Returns:
PromiseEx<TResult1 | TResult2>
Like then()
but immediately invoke callbacks, if this PromiseEx
is already resolved / rejected.
-
cause:
Promise<any>
Could be thrown from .value
and .wait(timeout)
of PromiseEx
-
output:
string | Nil | PrintMethod
β can be:- a
(timeMs, sinceMs) => void
- a
string
- print labelled result withtiming.defaultPrint()
, defaults to console.log
- a
-
promise:
T
-
Returns:
T
β result offn()
Measures time of execution of executeFn()
. Works on async function and Promise too.
const result = timing('read', () => {
const data = fs.readFileSync('xxx');
const decrypted = crypto.decrypt(data, key);
return decrypt;
})
// get result
// meanwhile, console prints "[read] took 120ms"
Or with custom logger
const print = (ms) => console.log(`[timing] fetching took ${ms}ms`)
const result = await timing(print, async () => {
const resp = await fetch('/user/xxx');
const user = await resp.json();
return user;
})
-
fn:
(defer: Tap<[]> & { silent: Tap<[]>; }) => Ret
-
Returns:
Ret
This is a wrapper of fnQueue
, inspired by golang's defer
keyword.
You can add dispose callbacks to a stack, and they will be invoked in finally
stage.
No more try catch finally
hells!
For sync functions:
// sync
const result = withDefer((defer) => {
const file = openFileSync('xxx')
defer(() => closeFileSync(file)) // <- register callback
const parser = createParser()
defer(() => parser.dispose()) // <- register callback
return parser.parse(file.readSync())
})
For async functions, use withAsyncDefer
// async
const result = await withAsyncDefer(async (defer) => {
const file = await openFile('xxx')
defer(async () => await closeFile(file)) // <- defer function can be async now!
const parser = createParser()
defer(() => parser.dispose()) // <-
return parser.parse(await file.read())
})
If you want to suppress the callbacks' throwing, use defer.silent
defer.silent(() => closeFile(file)) // will never throws
Refer to TypeScript using syntax,
TC39 Explicit Resource Management and GoLang's defer
keyword.
-
fn:
(defer: Tap<[]> & { silent: Tap<[]>; }) => Ret
-
Returns:
Ret
Same as withDefer, but this returns a Promise, and supports async callbacks.
-
source:
ModuleLoaderSource<T>
-
resolve:
(query: string, ctx: { load(target: string): PromiseEx<T>; noCache<T>(value: T): T; }) => MaybePromise<T>
β You must implement a loader function. It parsequery
and returns the module content.- It could be synchronous or asynchronous, depends on your scenario.
- You can use
load()
fromctx
to load dependencies. Example:await load("common")
orload("common").value
- All queries are cached by default. To bypass it, use
ctx.noCache
. Example:return noCache("404: not found")
-
cache?:
ModuleLoaderCache<any>
-
All-in-one ModuleLoader, support both sync and async mode, can handle circular dependency problem.
const loader = new ModuleLoader({
// sync example
resolve(query, { load }) {
if (query === 'father') return 'John'
if (query === 'mother') return 'Mary'
// simple alias: just `return load('xxx')`
if (query === 'mom') return load('mother')
// load dependency
// - `load('xxx').value` for sync, don't forget .value
// - `await load('xxx')` for async
if (query === 'family') return `${load('father').value} and ${load('mother').value}`
// always return something as fallback
return 'bad query'
}
})
console.log(loader.load('family').value) // don't forget .value
const loader = new ModuleLoader({
// async example
async resolve(query, { load }) {
if (query === 'father') return 'John'
if (query === 'mother') return 'Mary'
// simple alias: just `return load('xxx')`
if (query === 'mom') return load('mother')
// load dependency
// - `await load('xxx')` for async
// - no need `.value` in async mode
if (query === 'family') return `${await load('father')} and ${await load('mother')}`
// always return something as fallback
return 'bad query'
}
})
console.log(await loader.load('family')) // no need `.value` with `await`
π show members of ModuleLoader
Β»
- Type:
ModuleLoaderCache<{ dependencies?: string[] | undefined; promise: PromiseEx<T>; }>
-
query:
string
-
Returns:
PromiseEx<T>
fetch a module
-
query:
string
-
deep?:
boolean
-
Returns:
PromiseEx<string[]>
get all direct dependencies of a module.
note: to get reliable result, this will completely load the module and deep dependencies.
-
query:
string
-
queryStack:
string[]
The circular dependency Error that ModuleLoader
might throw.
π show members of CircularDependencyError
Β»
- Type:
string
the module that trying to be loaded.
- Type:
string[]
the stack to traceback the loading progress.
- Type:
string
always 'CircularDependencyError'
-
keyword:
string
-
Returns:
{ test, filter, filterEx }
-
test:
(record: any) => number
β test one record and tell if it matches.the
record
could be a string, array and object(only values will be tested).will return
0
for not matched,1
for fuzzy matched,> 1
for partially accurately matched -
filter:
FilterFunction
β filter a list / collection, and get the sorted search result.returns a similarity-sorted array of matched values.
also see
filterEx
if want more information -
filterEx:
FilterExFunction
β filter a list / collection, and get the sorted search result with extra information.returns a similarity-sorted array of
{ value, score, index, key }
.also see
filter
if you just want the values.
-
Simple utility to start searching
// note: items can be object / array / array of objects ...
const items = ['Alice', 'Lichee', 'Bob'];
const result = getSearchMatcher('lic').filter(items);
// -> ['Lichee', 'Alice']
-
x:
any
-
y:
any
-
Returns:
boolean
the Object.is
algorithm
-
objA:
any
-
objB:
any
-
depth?:
number
β defaults to 1 -
Returns:
boolean
-
argumentNames:
NameArray<ARGS>
β astring[]
of argument names -
functionBody:
string
β the function body -
options?:
{ async?: boolean | undefined; }
-
async?:
boolean
β set totrue
if the code containsawait
, the new function will be an async function
-
async?:
-
Returns:
Fn<RESULT, ARGS>
like new Function
but with more reasonable options and api
- Returns:
void
-
a:
number
β The first number. -
b:
number
β The second number. -
epsilon?:
number
β The maximum difference allowed between the two numbers. Defaults to 0.001. -
Returns:
boolean
Determines if two numbers are approximately equal within a given epsilon.
-
x:
number
β The x-coordinate of the point. -
y:
number
β The y-coordinate of the point. -
rect:
RectLike
β The rectangle to check against.-
x:
number
-
y:
number
-
width:
number
-
height:
number
-
-
Returns:
boolean
Determines whether a point (x, y) is inside a rectangle.
-
rect1:
Nil | RectLike
β The first rectangle to compare. -
rect2:
Nil | RectLike
β The second rectangle to compare. -
epsilon?:
number
β The maximum difference allowed between the values of the rectangles' properties. -
Returns:
boolean
Determines whether two rectangles are equal.
-
rect:
RectLike
β The first rectangle.-
x:
number
-
y:
number
-
width:
number
-
height:
number
-
-
bounds:
RectLike
β The second rectangle.-
x:
number
-
y:
number
-
width:
number
-
height:
number
-
-
Returns:
RectLike
β The intersection rectangle. Can be accepted byDOMRect.fromRect(.)
-
x:
number
-
y:
number
-
width:
number
-
height:
number
-
Calculates the intersection of two rectangles.
-
value:
OneOrMany<T>
-
Returns:
T[]
Input anything, always return an array.
- If the input is a single value that is not an array, wrap it as a new array.
- If the input is already an array, it returns a shallow copy.
- If the input is an iterator, it is equivalent to using
Array.from()
to process it.
Finally before returning, all null
and undefined
will be omitted
-
iterator:
Nil | Iterable<T>
-
predicate:
Predicate<T>
-
Returns:
T | undefined
Like Array#find
, but the input could be a Iterator (for example, from generator, Set
or Map
)
-
iterator:
Nil | Iterable<T>
-
initial:
U
-
reducer:
(agg: U, item: T, index: number) => U
-
Returns:
U
Like Array#reduce
, but the input could be a Iterator (for example, from generator, Set
or Map
)
-
iterator:
Nil | Iterable<T>
-
Returns:
T | undefined
Take the first result from a Iterator
-
collection:
Nil | CollectionOf<T>
-
item:
T
-
Returns:
boolean
input an array / Set / Map / WeakSet / WeakMap / object etc, check if it contains the item
-
objOrArray:
any
-
iter:
(value: any, key: any, whole: any) => any
-
Returns:
void
a simple forEach iterator that support both Array | Set | Map | Object | Iterable
as the input
-
str:
string
-
Returns:
number
Quickly compute string hash with cyrb53 algorithm
-
basicName:
string
-
existingVariables?:
CollectionOf<string>
-
Returns:
string
input anything weird, get a valid variable name
optionally, you can give a existingVariables
to avoid conflicting -- the new name might have a numeric suffix
getVariableName('foo-bar') // -> "fooBar"
getVariableName('123abc') // -> "_123abc"
getVariableName('') // -> "foobar"
getVariableName('name', ['name', 'age']) // -> "name2"
-
text1:
string | number | null | undefined
-
text2:
string | number | null | undefined
-
brackets?:
string | [string, string]
β defaults to[" (", ")"]
-
Returns:
string
Add bracket (parenthesis) to text
-
bracket("c_name", "Column Name")
=>"c_name (Column Name)"
-
bracket("Column Name", "c_name")
=>"Column Name (c_name)"
If one parameter is empty, it returns the other one:
-
bracket("c_name", null)
=>"c_name"
-
bracket(null, "c_name")
=>"c_name"
-
obj:
any
-
Returns:
boolean
Tell if obj
is null or undefined
-
obj:
any
-
Returns:
false | "array" | "object"
Tell if obj
is Array, Object or other(false
)
-
sth:
any
-
Returns:
boolean