yon-utils v0.1.23
yon-utils
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!
QuickStart
All modules are shipped as ES modules and tree-shakable.
via package manager
npm install yon-utilsvia import within
<script type="module">import { elt } from "https://unpkg.com/yon-utils"
ToC
| 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 |
🧩 dom/clipboard
writeClipboard(text)
text:
stringReturns:
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.
readClipboard(timeout?)
timeout?:
number— default 1500Returns:
Promise<string>
read clipboard text.
if user rejects or hesitates about the permission for too long, this will throw an Error.
🧩 dom/clsx
clsx(...args)
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
🧩 dom/elt
elt(tagName, attrs, ...children)
tagName:
string— for example"div"or"button.my-btn"attrs:
any— attribute values to be set. beware:onClickand afunctionvalue, will be handled byaddEventListener()!onClickoronClick.capturewill make it capturestylevalue could be a string or objectclassvalue could be a string, object or array, and will be process byclsx()classNameis 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>
🧩 dom/keyboard
modKey(ev)
ev:
KeyboardEventLikectrlKey?:
booleanmetaKey?:
booleanshiftKey?:
booleanaltKey?:
boolean
Returns:
number
get Modifier Key status from a Event
Remark
- use
modKey.Modto indicate if the key is⌘(Cmd) on Mac, orCtrlon Windows/Linux - use
|(or operator) to combine modifier keys. see example below.
Example
if (modKey(ev) === (modKey.Mod | modKey.Shift) && ev.code === 'KeyW') {
// Ctrl/Cmd + Shift + W, depends on the OS
}🧩 dom/mouseMove
startMouseMove({ initialEvent, onMove, onEnd })
__0:
MouseMoveInitOptionsinitialEvent:
MouseEvent | PointerEventonMove?:
(data: MouseMoveInfo) => voidonEnd?:
(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)
Example
button.addEventListener('pointerdown', event => {
event.preventDefault();
startMouseMove({
initialEvent: event,
onMove({ deltaX, deltaY }) { ... },
onEnd({ deltaX, deltaY }) { ... },
});
});🧩 flow/flow
delay(milliseconds)
milliseconds:
numberReturns:
Promise<void>
debouncePromise(fn)
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.
🧩 flow/fnQueue
fnQueue(async?, reversed?, error?)
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 errorsReturns:
FnQueue<ARGS, void>tap:
Tap<ARGS> & { silent: Tap<ARGS>; }— add functions to queue. see example. use tap.silent(fns) to ignore errorstapSilent:
Tap<ARGS>— add functions to queue, but silently ignore their errors (identical to tap.silent)call:
(...args: ARGS) => RET— clear the queue, execute functionsqueue:
{ silent?: boolean | undefined; fn: Fn<any, ARGS>; }[]— the queued functions
Example
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
}🧩 flow/makeAsyncIterator
makeAsyncIterator()
- 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.
Example
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);
}🧩 flow/makeEffect
makeEffect(fn, isEqual?)
fn:
(input: T, previous: T | undefined) => void | (() => void)isEqual?:
(x: T, y: T) => booleanReturns:
{ (input: T): void; cleanup(): void; readonly value: T | undefined; }cleanup:
() => void— invoke last cleanup function, and resetvalueto undefinedvalue?:
NonNullable<T>— get last received value, orundefinedif 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.
Example
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🧩 flow/promise
maybeAsync(input)
input:
T | Promise<T> | (() => T | Promise<T>)— your sync/async function to run, or just a valueReturns:
PromiseEx<Awaited<T>>— a crafted Promise that exposes{ status, value, reason }, whosestatuscould 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
.valuewhich follows fail-fast mentalityloading:
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
PromisePendingErrorwill be thrownthenImmediately:
<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.
makePromise()
- 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, aPromisePendingErrorwill be thrown.status— could be"pending" | "fulfilled" | "rejected".resultand.reason.value— fail-safe get result (or cause an Error from rejection, or cause aPromisePendingErrorif still pending)
Example
const handler = makePromise();
doSomeRequest(..., result => handler.resolve(result));
// wait with timeout
const result = await handler.wait(1000);
// or just await
const result = await handler;new PromiseEx(executor)
- 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
PromiseEx # status
- Type:
"pending" | "fulfilled" | "rejected"
PromiseEx # reason
- Type:
any
if rejected, get the reason.
PromiseEx # result
- Type:
T | undefined
get result, or nothing if not fulfilled.
note: you might need .value which follows fail-fast mentality
PromiseEx # loading
- Type:
boolean
equivalent to .status === "pending"
PromiseEx # value
- Type:
T | undefined
fail-fast mentality, safely get the result.
- if pending, throw
new PromisePendingError(this) - if rejected, throw
.reason - if fulfilled, get
.result
PromiseEx # wait(timeout)
timeout:
numberReturns:
Promise<T>
wait for resolved / rejected.
optionally can set a timeout in milliseconds. if timeout, a PromisePendingError will be thrown
PromiseEx # thenImmediately(onfulfilled?, onrejected?)
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.
new PromisePendingError(cause)
- cause:
Promise<any>
Could be thrown from .value and .wait(timeout) of PromiseEx
PromisePendingError # cause
- Type:
Promise<any>
🧩 flow/timing
timing(output, promise)
output:
string | Nil | PrintMethod— can be:- a
(timeMs, sinceMs) => void - a
string- print labelled result withtiming.defaultPrint(), defaults to console.log
- a
promise:
TReturns:
T— result offn()
Measures time of execution of executeFn(). Works on async function and Promise too.
Example
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;
})🧩 flow/withDefer
withDefer(fn)
fn:
(defer: Tap<[]> & { silent: Tap<[]>; }) => RetReturns:
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 throwsRemark
Refer to TypeScript using syntax,
TC39 Explicit Resource Management and GoLang's defer keyword.
withAsyncDefer(fn)
fn:
(defer: Tap<[]> & { silent: Tap<[]>; }) => RetReturns:
Ret
Same as withDefer, but this returns a Promise, and supports async callbacks.
🧩 manager/moduleLoader
new ModuleLoader(source)
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 parsequeryand returns the module content.- It could be synchronous or asynchronous, depends on your scenario.
- You can use
load()fromctxto 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.
Example in Sync
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 .valueExample in Async
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`ModuleLoader # cache
- Type:
ModuleLoaderCache<{ dependencies?: string[] | undefined; promise: PromiseEx<T>; }>
ModuleLoader # load(query)
query:
stringReturns:
PromiseEx<T>
fetch a module
ModuleLoader # getDependencies(query, deep?)
query:
stringdeep?:
booleanReturns:
PromiseEx<string[]>
get all direct dependencies of a module.
note: to get reliable result, this will completely load the module and deep dependencies.
new CircularDependencyError(query, queryStack)
query:
stringqueryStack:
string[]
The circular dependency Error that ModuleLoader might throw.
CircularDependencyError # query
- Type:
string
the module that trying to be loaded.
CircularDependencyError # queryStack
- Type:
string[]
the stack to traceback the loading progress.
CircularDependencyError # name
- Type:
string
always 'CircularDependencyError'
🧩 manager/simpleSearch
getSearchMatcher(keyword)
keyword:
stringReturns:
{ test, filter, filterEx }test:
(record: any) => number— test one record and tell if it matches.the
recordcould be a string, array and object(only values will be tested).will return
0for not matched,1for fuzzy matched,> 1for partially accurately matchedfilter:
FilterFunction— filter a list / collection, and get the sorted search result.returns a similarity-sorted array of matched values.
also see
filterExif want more informationfilterEx:
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
filterif you just want the values.
Simple utility to start searching
Example
// note: items can be object / array / array of objects ...
const items = ['Alice', 'Lichee', 'Bob'];
const result = getSearchMatcher('lic').filter(items);
// -> ['Lichee', 'Alice']🧩 type/compare
is(x, y)
x:
anyy:
anyReturns:
boolean
the Object.is algorithm
shallowEqual(objA, objB, depth?)
objA:
anyobjB:
anydepth?:
number— defaults to 1Returns:
boolean
🧩 type/function
newFunction(argumentNames, functionBody, options?)
argumentNames:
NameArray<ARGS>— astring[]of argument namesfunctionBody:
string— the function bodyoptions?:
{ async?: boolean | undefined; }- async?:
boolean— set totrueif 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
noop()
- Returns:
void
🧩 type/geometry
approx(a, b, epsilon?)
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.
isInsideRect(x, y, rect)
x:
number— The x-coordinate of the point.y:
number— The y-coordinate of the point.rect:
RectLike— The rectangle to check against.x:
numbery:
numberwidth:
numberheight:
number
Returns:
boolean
Determines whether a point (x, y) is inside a rectangle.
isRectEqual(rect1, rect2, epsilon?)
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.
getRectIntersection(rect, bounds)
rect:
RectLike— The first rectangle.x:
numbery:
numberwidth:
numberheight:
number
bounds:
RectLike— The second rectangle.x:
numbery:
numberwidth:
numberheight:
number
Returns:
RectLike— The intersection rectangle. Can be accepted byDOMRect.fromRect(.)x:
numbery:
numberwidth:
numberheight:
number
Calculates the intersection of two rectangles.
🧩 type/iterable
toArray(value)
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
find(iterator, predicate)
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)
reduce(iterator, initial, reducer)
iterator:
Nil | Iterable<T>initial:
Ureducer:
(agg: U, item: T, index: number) => UReturns:
U
Like Array#reduce, but the input could be a Iterator (for example, from generator, Set or Map)
head(iterator)
iterator:
Nil | Iterable<T>Returns:
T | undefined
Take the first result from a Iterator
contains(collection, item)
collection:
Nil | CollectionOf<T>item:
TReturns:
boolean
input an array / Set / Map / WeakSet / WeakMap / object etc, check if it contains the item
forEach(objOrArray, iter)
objOrArray:
anyiter:
(value: any, key: any, whole: any) => anyReturns:
void
a simple forEach iterator that support both Array | Set | Map | Object | Iterable as the input
🧩 type/string
stringHash(str)
str:
stringReturns:
number
Quickly compute string hash with cyrb53 algorithm
getVariableName(basicName, existingVariables?)
basicName:
stringexistingVariables?:
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
Example
getVariableName('foo-bar') // -> "fooBar"
getVariableName('123abc') // -> "_123abc"
getVariableName('') // -> "foobar"
getVariableName('name', ['name', 'age']) // -> "name2"bracket(text1, text2, brackets?)
text1:
string | number | null | undefinedtext2:
string | number | null | undefinedbrackets?:
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"
🧩 type/types
isNil(obj)
obj:
anyReturns:
boolean
Tell if obj is null or undefined
isObject(obj)
obj:
anyReturns:
false | "array" | "object"
Tell if obj is Array, Object or other(false)
isThenable(sth)
sth:
anyReturns:
boolean
1 year ago
1 year ago
1 year ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago