Skip to content

Commit

Permalink
usePromise defaultValue (via config)
Browse files Browse the repository at this point in the history
  • Loading branch information
arietrouw committed Oct 30, 2024
1 parent 6e083bd commit 9ae492f
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 31 deletions.
1 change: 1 addition & 0 deletions packages/promise/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './model.ts'
export * from './useAtomicPromise.ts'
export * from './usePromise.ts'
22 changes: 22 additions & 0 deletions packages/promise/src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Represents the state of a promise.
*
* @type {UsePromiseState}
* @property {'pending'} pending - The promise is still pending.
* @property {'rejected'} rejected - The promise has been rejected.
* @property {'resolved'} resolved - The promise has been resolved.
*/
export type UsePromiseState = 'pending' | 'rejected' | 'resolved'

/**
* Configuration options for the usePromise hook.
*
* @template TResult - The type of the result value.
*
* @property {string} [debug] - Optional debug string for logging purposes.
* @property {TResult} [defaultValue] - Optional default value to be used before the promise resolves.
*/
export interface UsePromiseConfig<TResult> {
debug?: string
defaultValue?: TResult
}
20 changes: 13 additions & 7 deletions packages/promise/src/useAtomicPromise.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
// Inspired from https://github.com/bsonntag/react-use-promise

import { Mutex } from 'async-mutex'
import type { DependencyList } from 'react'

import type { State } from './usePromise.ts'
import type { UsePromiseConfig, UsePromiseState } from './model.ts'
import { usePromise } from './usePromise.ts'

const mutexDictionary: Record<string, Mutex> = {}

/**
* useAtomicPromise - The same as usePromise, but ensures that only one promise is running at a time.
* A custom hook that ensures a promise is executed atomically, using a mutex to prevent concurrent executions.
*
* @template TResult - The type of the result that the promise resolves to.
* @param {string} name - A unique name for the mutex to ensure atomic execution.
* @param {() => Promise<TResult | undefined>} promise - A function that returns the promise to be executed.
* @param {DependencyList} dependencies - An array of dependencies that will trigger the promise execution when changed.
* @param {UsePromiseConfig<TResult>} [config] - Optional configuration for the promise execution.
* @returns {[TResult | undefined, Error | undefined, UsePromiseState | undefined]}
* An array containing the result of the promise, any error that occurred, and the state of the promise.
*/
export const useAtomicPromise = <TResult>(
name: string,
promise: () => Promise<TResult | undefined>,
dependencies: DependencyList,
debug: string | undefined = undefined,
): [TResult | undefined, Error | undefined, State | undefined] => {
config?: UsePromiseConfig<TResult>,
): [TResult | undefined, Error | undefined, UsePromiseState | undefined] => {
mutexDictionary[name] = mutexDictionary[name] ?? new Mutex()
return usePromise(() => mutexDictionary[name].runExclusive(promise), dependencies, debug)
return usePromise(() => mutexDictionary[name].runExclusive(promise), dependencies, config)
}
54 changes: 30 additions & 24 deletions packages/promise/src/usePromise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,61 +7,67 @@ import {
} from 'react'

import { usePromiseSettings } from './context/index.ts'

export enum State {
pending = 'pending',
rejected = 'rejected',
resolved = 'resolved',
}
import type { UsePromiseConfig, UsePromiseState } from './model.ts'

/**
* usePromise -
* A custom hook that manages the state of a promise, including its result, error, and state.
*
* @template TResult - The type of the result that the promise resolves to.
*
* @param {() => Promise<TResult | undefined>} promise - A function that returns a promise.
* @param {DependencyList} dependencies - An array of dependencies that will trigger the promise to be re-evaluated when changed.
* @param {UsePromiseConfig<TResult>} [config] - Optional configuration for the hook.
* @param {TResult} [config.defaultValue] - The default value to be used before the promise resolves.
* @param {boolean} [config.debug] - If true, debug information will be logged to the console.
*
* @returns {[TResult | undefined, Error | undefined, UsePromiseState | undefined]}
* An array containing the result of the promise, any error that occurred, and the current state of the promise.
*/
export const usePromise = <TResult>(
promise: () => Promise<TResult | undefined>,
dependencies: DependencyList,
debug: string | undefined = undefined,
): [TResult | undefined, Error | undefined, State | undefined] => {
config?: UsePromiseConfig<TResult>,
): [TResult | undefined, Error | undefined, UsePromiseState | undefined] => {
const { logErrors } = usePromiseSettings()
const [result, setResult] = useState<TResult>()
const [result, setResult] = useState<TResult | undefined>(config?.defaultValue)
const [error, setError] = useState<Error>()
const [state, setState] = useState<State>(State.pending)
const [state, setState] = useState<UsePromiseState>('pending')
const mutex = useMemo(() => {
return new Mutex()
}, [])

if (debug) console.log(`usePromise [${debug}]: started [${typeof promise}]`)
if (config?.debug) console.log(`usePromise [${config?.debug}]: started [${typeof promise}]`)

const promiseMemo: Promise<TResult | undefined> | undefined = useMemo(() => {
try {
if (debug) console.log(`usePromise [${debug}]: re-memo [${typeof promise}]`)
setState(State.pending)
if (config?.debug) console.log(`usePromise [${config?.debug}]: re-memo [${typeof promise}]`)
setState('pending')
return promise?.()
} catch (ex) {
const error = ex as Error
if (logErrors) console.error(`usePromise-memo: ${error}`)
if (debug) console.log(`usePromise [${debug}]: useMemo rejection [${typeof promise}]`)
if (config?.debug) console.log(`usePromise [${config?.debug}]: useMemo rejection [${typeof promise}]`)
setResult(undefined)
setError(error)
setState(State.rejected)
setState('rejected')
}
}, dependencies)

if (debug) console.log(`usePromise [${debug}] Main Function`)
if (config?.debug) console.log(`usePromise [${config?.debug}] Main Function`)

useEffect(() => {
let loaded = true
if (debug) console.log(`usePromise [${debug}] useEffect`)
if (config?.debug) console.log(`usePromise [${config?.debug}] useEffect`)
mutex
?.acquire()
.then(() => {
promiseMemo
?.then((payload) => {
if (debug) console.log(`usePromise [${debug}] then`)
if (config?.debug) console.log(`usePromise [${config?.debug}] then`)
if (loaded) {
setResult(payload)
setError(undefined)
setState(State.resolved)
setState('resolved')
}
mutex?.release()
})
Expand All @@ -71,7 +77,7 @@ export const usePromise = <TResult>(
if (loaded) {
setResult(undefined)
setError(error)
setState(State.rejected)
setState('resolved')
}
mutex?.release()
})
Expand All @@ -82,15 +88,15 @@ export const usePromise = <TResult>(
if (loaded) {
setResult(undefined)
setError(error)
setState(State.rejected)
setState('rejected')
}
mutex?.release()
})
return () => {
loaded = false
if (debug) console.log(`usePromise [${debug}] useEffect callback`)
if (config?.debug) console.log(`usePromise [${config?.debug}] useEffect callback`)
}
}, [...dependencies, promiseMemo])
if (debug) console.log(`usePromise [${debug}] returning ${JSON.stringify([result, error, state], null, 2)}`)
if (config?.debug) console.log(`usePromise [${config?.debug}] returning ${JSON.stringify([result, error, state], null, 2)}`)
return [result, error, state]
}

0 comments on commit 9ae492f

Please sign in to comment.