yon-utils
v0.2.2
Published
Some utils that I repeated too many times. DRY!
Readme
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 ... randomStr | nanoid <some lodash-like functions> | lodash
There are also some interesting original utils like shallowEqual / newFunction / toArray / getVariableName etc. Feel free to explore!
Other cool third-party libraries:
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
| category | exports | | --- | ------- | | convertor | methods: jsonSchemaToTypeScript | | dom | methods: clsx / elt | | execute | methods: newFunction / noop / retry / pollUntil | | flow | methods: fnQueue / makeAsyncIterator / makeEffect / withDefer / withAsyncDefer / createWorkerHandler / createWorkerDispatcher | | interaction | methods: writeClipboard / readClipboard / modKey / startMouseMove vars: IS_MAC / MOD_KEY / MOD_KEY_LABEL types: KeyboardEventLike / MouseMoveInfo / MouseMoveInitOptions | | iterable | methods: toArray / find / findMap / mapFilter / reduce / head / contains / forEach / filterMap types: OneOrMany / Predicate / PredicateAndMap / CollectionOf / IterItem | | manager | methods: getObjectUniqueKey classes: ModuleLoader / CircularDependencyError types: ModuleLoaderCache / ModuleLoaderSource | | math | methods: isInsideRect / isRectEqual / getRectIntersection / getRectUnion / approx / clamp / lerp / easeOut / randomNum types: RectLike / RectLikeXYWH / RectLikeLTWH | | promise | methods: delay / debouncePromise / maybeAsync / makePromise classes: PromiseEx / PromisePendingError / SwappablePromise types: ImperativePromiseEx / SwappablePromiseExecutor | | string | methods: stringHash / getVariableName / bracket / randomStr / nanoid | | value | methods: shallowEqual / tryParseJSON / isNil / isObject / isThenable types: MaybePromise / Falsy / Nil / Fn |
🧩 convertor/json-schema
jsonSchemaToTypeScript(schema, indent?)
schema:
JSONSchemaindent?:
numberreturns:
string
a simple util to translate JSON Schema to TypeScript type declaration, with shortest comment (single line if possible)
🧩 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— e.g."button"or"button.my-btn#myId"attrs:
any—class(processed by clsx),style(string/object),onClick(auto addEventListener)...children:
any[]— strings, numbers, nodesreturns:
HTMLElement
Simplified document.createElement with support for class/id shortcuts, event listeners, and JSX.
Works with JSX: /** @jsx elt */
Remarks
- Event modifier:
onClick_capture/onClick_once/onClick_selfclassNameis alias ofclass, supports{ 'my-button': true, 'is-primary': XXX }
Example
elt('button.myButton', { onclick: () => alert('hi') }, 'Click Me!') Click Me
🧩 execute/function
newFunction(argumentNames, functionBody, options?)
argumentNames:
NameArray<ARGS>— astring[]of argument namesfunctionBody:
string— the function bodyoptions?:
{ async? }- async?:
boolean | undefined— 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
The nothing-to-do function
🧩 execute/retry
retry(fn, opts?)
fn:
() => T | Nil | Promise<T | Nil>— Function to retryopts?:
{ duration?, interval?, signal? }duration?:
number | undefinedinterval?:
number | undefinedsignal?:
AbortSignal | undefined
returns:
Promise<T>— Promise resolving to the first successful result
Retries fn() until it returns a non-null value without throwing.
Throws when timeout or aborted.
pollUntil(fn, opts?)
fn:
() => T | Nil | Promise<T | Nil>— - Function to retryopts?:
{ duration?, interval?, signal? }duration?:
number | undefinedinterval?:
number | undefinedsignal?:
AbortSignal | undefined
returns:
Promise<T>
Alias for retry. Polls a function until it returns a non-null value, or throws if timeout or aborted.
🧩 flow/fnQueue
fnQueue()
returns:
{ tap, tapSilent, call, queue }tap:
AddCallbacks<Args>— add one or more functions.tapSilent:
AddCallbacks<Args>— add functions, and will silently ignore their errorscall:
(...args: Args) => void— run functions. if fnQueue is async, returns Promisequeue:
{ silent?: boolean | undefined; fn: Fn<any, Args>; }[]— the queued functions
Store a list of functions, and execute them in order.
- Use case: 🧹 disposer (clean resources) / ⚡ event emitter / 🪢 tapable-like middleware
- Defaults: sync, FIFO, errors will abort
Use decorators or options, to customize a fnQueue:
fnQueue.async()to create async queue -- thecall()will return a Promise instead.fnQueue.filo()to create FILO queue.fnQueue.onetime()to clear the queue after each call.fnQueue({ error: 'ignore' })to ignore errors.
Options can be combined, like fnQueue.async.onetime() -- see example below.
Example
// create an async fnQueue with options ...
const disposer = fnQueue.async.onetime({ error: 'ignore' });
try {
const srcFile = await openFile(path1);
disposer.tap(() => srcFile.close());
const dstFile = await openFile(path2);
disposer.tap(() => dstFile.close());
await copyData(srcFile, dstFile);
} finally {
await disposer.call();
}async:
Factory<Promise<void>>async:
Factory<Promise<void>>filo:
Factory<Promise<void>>— change execution order to FILO (first-in, last-out, like a stack)onetime:
Factory<Promise<void>>— after each call, clear the queue
filo:
Factory<void>— change execution order to FILO (first-in, last-out, like a stack)async:
Factory<Promise<void>>filo:
Factory<void>— change execution order to FILO (first-in, last-out, like a stack)onetime:
Factory<void>— after each call, clear the queue
onetime:
Factory<void>— after each call, clear the queueasync:
Factory<Promise<void>>filo:
Factory<void>— change execution order to FILO (first-in, last-out, like a stack)onetime:
Factory<void>— after each call, clear the queue
🧩 flow/makeAsyncIterator
makeAsyncIterator()
returns:
{ write(value: T): void; end(error?: any): void; } & AsyncIterableIterator<T>write:
(value: T) => voidend:
(error?: any) => void
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:
{ cleanup, value }cleanup:
() => void— invoke last cleanup function, and resetvalueto undefinedvalue?:
T | undefined— 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/withDefer
withDefer(fn)
fn:
(defer: AddCallbacks<[]>) => Retreturns:
Ret
Get rid of try catch finally hells!
Use defer(callback) to clean up resources, and they will run in finally stage.
Works on both sync and async procedures.
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())
})Error handling
If one callback throws, rest callbacks still work. And you get the last error thrown.
To suppress a callback's throwing, use defer.silent(callback)
defer.silent(() => closeFile(file)) // will never throwsRemarks
Refer to TypeScript using syntax,
TC39 Explicit Resource Management and GoLang's defer keyword.
withAsyncDefer(fn)
fn:
(defer: AddCallbacks<[]>) => Retreturns:
Ret
Same as withDefer, but this returns a Promise, and supports async callbacks.
🧩 flow/worker-rpc
createWorkerHandler(methods)
methods:
T— Object containing method implementations that can be called remotelyreturns:
(payload: RpcPayload) => void— A handler function that processes incoming RPC payloads
Creates a handler function for processing RPC requests in a Worker.
Example
// In your worker file:
import { createWorkerHandler } from './worker-rpc';
const handler = createWorkerHandler({
async getData(id) {
// implementation
return result;
}
});
self.onmessage = (e) => {
if (e.data?.type === 'myCall') handler(e.data.payload);
}createWorkerDispatcher(postMessage)
postMessage:
(payload: RpcPayload, transferable: Transferable[]) => void— Function that sends messages to the worker (typically worker.postMessage)returns:
T— A proxy object where each property access creates a function that calls the corresponding method in the worker
Creates a proxy object that dispatches method calls to a Worker via RPC.
Example
// In your main thread:
import { createWorkerDispatcher } from './worker-rpc';
const worker = new Worker('path/to/worker.js');
const api = createWorkerDispatcher<MyWorkerAPI>((payload, transferable) =>
worker.postMessage({
type: 'myCall',
payload,
}, transferable)
);
// Now you can call worker methods as if they were local:
// No need to worry about initiating, it automatically wait for the worker to be ready
const result = await api.getData(123);🧩 interaction/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.
🧩 interaction/keyboard
modKey(ev)
ev:
KeyboardEventLikereturns:
number
get Modifier Key status from a Event
Remarks
- 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
}None:
0Ctrl:
1Cmd:
2Shift:
4Alt:
8Mod:
1 | 2— equals toCtrlorCmd, depending on the OS
IS_MAC
booleanMOD_KEY
the proper way to get modKey status from KeyboardEvent
"metaKey" | "ctrlKey"MOD_KEY_LABEL
the proper label of modKey. eg: ⌘ for Mac, Ctrl for Windows/Linux
"⌘" | "Ctrl"interface KeyboardEventLike
ctrlKey?:
boolean | undefinedmetaKey?:
boolean | undefinedshiftKey?:
boolean | undefinedaltKey?:
boolean | undefined
🧩 interaction/mouseMove
startMouseMove({ initialEvent, onMove, onEnd })
__0:
MouseMoveInitOptionsreturns:
Promise<MouseMoveInfo>— a Promise with final position when user releases button
Powerful tool to implement any drag / resize action easily!
While dragging, it tracks the cursor's movement and reports to onMove(...).
And when user releases the button, it will call onEnd(...).
the element needs
touch-action: nonestyle to prevent scrolling on mobileuse within
pointerdowneventevent.preventDefault()is necessary too
Example
button.style.touchAction = 'none' // CSS touch-action: none
button.addEventListener('pointerdown', event => {
event.preventDefault();
startMouseMove({
initialEvent: event,
onMove({ deltaX, deltaY }) { ... },
onEnd({ deltaX, deltaY }) { ... },
});
});interface MouseMoveInfo
clientX:
numberclientY:
numberdeltaX:
numberdeltaY:
numberduration:
number— in milliseconds, sincestartMouseMovecalledevent:
MouseEvent | PointerEventpointerId:
number | falsecancelled?:
boolean | undefined
interface MouseMoveInitOptions
initialEvent:
MouseEvent | PointerEventonMove?:
((data: MouseMoveInfo) => void) | undefinedonEnd?:
((data: MouseMoveInfo) => void) | undefined
🧩 iterable/iterable
toArray(value)
value?:
OneOrMany<T>returns:
NonNullable<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)
findMap(iterator, map)
iterator?:
Nil | Iterable<Nil | T>map:
PredicateAndMap<T, U>returns:
U | undefined
Find the first item that matches the predicate, and then map it to a new value.
map function must return non-null value, otherwise it will be skipped.
mapFilter(iterator, map)
iterator?:
Nil | Iterable<Nil | T>map:
PredicateAndMap<T, U>returns:
U[]
Type-safe Shortcut for .map(fn).filter(val => !isNil(val))
map function must return non-null value, otherwise it will be skipped.
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:
Uiter:
(value: U[keyof U], key: keyof U, whole: U) => voidreturns:
void
a simple forEach iterator that support both Array | Set | Map | Object | Iterable as the input
when met plain object, it works like forIn of lodash.
type OneOrMany<T>
export type OneOrMany<T> = Iterable<T | Nil> | T | Niltype Predicate<T>
export type Predicate<T> = (value: T, index: number) => boolean;type PredicateAndMap<T, U>
export type PredicateAndMap<T, U> = (value: T, index: number) => U | Nil;filterMap(iterator, map)
iterator?:
Nil | Iterable<Nil | T>map:
PredicateAndMap<T, U>returns:
U[]
alias of mapFilter
type CollectionOf<T>
export type CollectionOf<T> =
| ReadonlyArray<T>
| Set<T> | Map<T, any>
| (T extends object ? WeakMap<T, any> | WeakSet<T> : never)
| (T extends string ? Record<T, any> : never)type IterItem<T>
export type IterItem<T> = T extends Iterable<infer U> ? U : never;🧩 manager/moduleLoader
new ModuleLoader<T>(source)
- source:
ModuleLoaderSource<T>
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'
type ModuleLoaderCache<T>
used by ModuleLoader
get:
(query: string) => T | undefinedset:
(query: string, value: T) => anydelete:
(query: string) => anyclear:
() => void
interface ModuleLoaderSource<T>
used by ModuleLoader
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> | undefined
🧩 manager/objectKey
getObjectUniqueKey(obj)
obj:
any— The object or function to get a unique key forreturns:
number | undefined— A unique numeric identifier, or undefined if input invalid
Gets a unique numeric key for an object. (value >= 1)
Uses a global WeakMap store to ensure the same object always returns the same key across calls.
Remarks
Create a new isolated store with getObjectUniqueKey.createStore(onCreate?)
- returns (obj) => key function
onCreate(obj, key)- optional, called when a new key is created.
Example
const obj = {};
getObjectUniqueKey(obj); // 1
getObjectUniqueKey(obj); // 1 (same key)
getObjectUniqueKey({}); // 2 (different object)- createStore:
<T = any>(onCreate?: ((obj: T, key: number) => void) | undefined) => ObjectKeyGetter
🧩 math/geometry
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.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 | undefined— The maximum difference allowed between the values of the rectangles' properties.returns:
boolean
Determines whether two rectangles are equal.
getRectIntersection(...rects)
...rects:
(Nil | RectLike)[]— The rectanglesreturns:
RectLike— The intersection rectangle. Can be passed toDOMRect.fromRect(.)
Calculates the intersection of rectangles.
getRectUnion(...rects)
...rects:
(Nil | RectLike)[]— The rectanglesreturns:
RectLike— The union rectangle. Can be passed toDOMRect.fromRect(.)
Calculates the union (out bounding box) of rectangles.
type RectLike
a interface that fits DOMRect and many other situation
export type RectLike = RectLikeXYWH | RectLikeLTWH;interface RectLikeXYWH
x:
numbery:
numberwidth:
numberheight:
number
interface RectLikeLTWH
left:
numbertop:
numberwidth:
numberheight:
number
🧩 math/number
approx(a, b, epsilon?)
a:
numberb:
numberepsilon?:
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.
clamp(n, min, max)
n:
numbermin:
numbermax:
numberreturns:
number
Clamp a number between min and max
lerp(min, max, t)
min:
numbermax:
numbert:
number— The interpolation value clamped between 0 and 1returns:
number
Linearly interpolate
Example
const value = lerp(0, 2, 0.5) // value will be 1easeOut(t)
t:
numberreturns:
number
simple ease-out function, useful for animation, tween, and fake progress bar
Example
// a fake progress bar never reach 100%
const sinceTime = Date.now()
const progress = 0.95 * easeOut((Date.now() - sinceTime) / 5000)randomNum(min, max)
min:
numbermax:
numberreturns:
number
Get a random number from [min, max)
🧩 promise/misc
delay(milliseconds)
milliseconds:
numberreturns:
Promise<void>
debouncePromise(fn)
fn:
() => Promise<T>— The function to be debounced.returns:
{ (): Promise<T>; clear(): void; }— The debounced function.- clear:
() => void— make next call always return new Promise
- clear:
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.
Remarks
use lodash's throttle() if you want to support things like trailing and cache-timeout (aka. interval) option.
🧩 promise/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"
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<T>(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 # isPending()
- returns:
boolean
PromiseEx # isFulfilled()
- returns:
boolean
PromiseEx # isRejected()
- returns:
boolean
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?:
number | undefinedreturns:
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?:
Nil | ((value: T) => TResult1 | PromiseLike<TResult1>)onrejected?:
Nil | ((reason: any) => TResult2 | PromiseLike<TResult2>)returns:
PromiseEx<TResult1 | TResult2>
Like then() but immediately invoke callbacks, if this PromiseEx
is already resolved / rejected.
resolve:
{ (): PromiseEx<void>; <T>(input: T): PromiseEx<Awaited<T>>; }— Creates a new resolved promise.Creates a new resolved promise for the provided value.
reject:
<T = never>(reason?: any) => PromiseEx<T>— Creates a new rejected promise for the provided reason.
new PromisePendingError(cause)
- cause:
Promise<any>
Could be thrown from .value and .wait(timeout) of PromiseEx
PromisePendingError # cause
- type:
Promise<any>
type ImperativePromiseEx<T>
export type ImperativePromiseEx<T> = PromiseEx<Awaited<T>> & {
resolve(result: T | PromiseLike<T>): void
reject(reason?: any): void
}🧩 promise/swappablePromise
interface SwappablePromiseExecutor
signal:
AbortSignal— triggered when current async process is replaced by anotherrun()isCurrent:
() => boolean— whether current process is the newest pending Promise. will becomefalsewhen resolved / rejected / "replaced by other Promise"throwIfNotCurrent:
() => void— throw if current process is not the newest pending Promise
new SwappablePromise<T>()
A swappable promise that allows dynamically changing the underlying promise being waited upon.
Useful in avoiding race conditions for UI updates.
This "SwappablePromise" will wait for the newest promise to resolve.
You can use run(fn) or swap(promise) to change the underlying promise.
example use cases:
- only display latest request's result, for multiple requests
- allow multiple submits, and old requests have "rollback" mechanism (use
run()to get context and the signal)
SwappablePromise # wait(timeout?)
timeout?:
number | undefinedreturns:
Promise<T>
wait for the underlying Promise resolved. the waiting target may be swapped, before resolved or rejected.
if no underlying Promise yet, will keep waiting.
SwappablePromise # run(executor)
executor:
(ctx: SwappablePromiseExecutor) => T | Promise<T>returns:
void
swap to a new async process, and pivot wait() & then() to wait for it
SwappablePromise # swap(promise)
promise:
T | Promise<T>returns:
void
swap to a new Promise. just alias of run(() => promise)
SwappablePromise # reset()
- returns:
void
reset to "pending" status
if someone is waiting, it will keep waiting, NOT aborted.
SwappablePromise # hasTarget()
- returns:
boolean
whether a underlying Promise is set and pending
SwappablePromise # isPending()
- returns:
boolean
whether is pending
SwappablePromise # isFulfilled()
- returns:
boolean
whether is fulfilled
SwappablePromise # isRejected()
- returns:
boolean
whether is rejected
SwappablePromise # then(onfulfilled?, onrejected?)
onfulfilled?:
((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefinedonrejected?:
((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefinedreturns:
Promise<TResult1 | TResult2>
wait for the underlying promise to resolve. the waiting target may be swapped, before resolved or rejected.
if no underlying Promise yet, will keep waiting.
🧩 string/string
stringHash(str)
str:
stringreturns:
number
Quickly compute string hash with cyrb53 algorithm
getVariableName(basicName, existingVariables?)
basicName:
stringexistingVariables?:
CollectionOf<string> | undefinedreturns:
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] | undefined— 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"
randomStr(size?, dict?)
size?:
number— defaults to 16dict?:
string— defaults to-\wsame as nanoidreturns:
string
Generate a random string
nanoid(size?, dict?)
size?:
number— - defaults to 16dict?:
string— - defaults to-\wsame as nanoidreturns:
string
alias of randomStr
🧩 value/compare
shallowEqual(objA, objB, depth?)
objA:
anyobjB:
anydepth?:
number— defaults to 1returns:
boolean
🧩 value/json
tryParseJSON(text, fallback)
text:
stringfallback:
Treturns:
T
safe parse JSON string, return fallback if failed
🧩 value/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
type MaybePromise<T>
export type MaybePromise<T> = Promise<T> | Ttype Falsy
TypeScript type presents all falsy value, including Nil, false, 0, ""
export type Falsy = null | undefined | false | 0 | "";type Nil
TypeScript type presents null or undefined
export type Nil = null | undefined;type Fn<RET, ARGS>
TypeScript type presents any function
export type Fn<RET = any, ARGS extends any[] = any[]> = (...args: ARGS) => RET;