import { detailedTypeof, RecursivePartial, Store } from '@zedux/core' import { api } from '../factories/api' import { getErrorPromiseState, getInitialPromiseState, getSuccessPromiseState, } from '../utils/promiseUtils' import { InjectorDeps, InjectPromiseConfig, InjectStoreConfig, PromiseState, } from '../types/index' import { injectEffect } from './injectEffect' import { injectMemo } from './injectMemo' import { injectStore } from './injectStore' import { injectRef } from './injectRef' import { AtomApi } from '../classes/AtomApi' import { injectWhy } from './injectWhy' /** * Create a memoized promise reference. Kicks off the promise immediately * (unlike injectEffect which waits a tick). Creates a store to track promise * state. This store's state shape is based off React Query: * * ```ts * { * data?: * error?: Error * isError: boolean * isLoading: boolean * isSuccess: boolean * status: 'error' | 'loading' | 'success' * } * ``` * * Returns an Atom API with `.store` and `.promise` set. * * The 2nd `deps` param is just like `injectMemo` - these deps determine when * the promise's reference should change. * * The 3rd `config` param can take the following options: * * - `dataOnly`: Set this to true to prevent the store from tracking promise * status and make your promise's `data` the entire state. * * - `initialState`: Set the initial state of the store (e.g. a placeholder * value before the promise resolves) * * - store config: Any other config options will be passed directly to * `injectStore`'s config. For example, pass `subscribe: false` to * prevent the store from reevaluating the current atom on update. * * ```ts * const promiseApi = injectPromise(async () => { * const response = await fetch(url) * return await response.json() * }, [url], { * dataOnly: true, * initialState: '', * subscribe: false * }) * ``` */ export const injectPromise: { ( promiseFactory: (controller?: AbortController) => Promise, deps: InjectorDeps, config: Omit & { dataOnly: true } & InjectStoreConfig ): AtomApi<{ Exports: Record Promise: Promise State: T Store: Store }> ( promiseFactory: (controller?: AbortController) => Promise, deps?: InjectorDeps, config?: InjectPromiseConfig & InjectStoreConfig ): AtomApi<{ Exports: Record Promise: Promise State: PromiseState Store: Store> }> } = ( promiseFactory: (controller?: AbortController) => Promise, deps?: InjectorDeps, { dataOnly, initialState, runOnInvalidate, ...storeConfig }: InjectPromiseConfig & InjectStoreConfig = {} ) => { const refs = injectRef({ counter: 0 } as { controller?: AbortController counter: number promise: Promise }) const store = injectStore( dataOnly ? initialState : getInitialPromiseState(initialState), storeConfig ) if ( runOnInvalidate && // injectWhy is an unrestricted injector - using it conditionally is fine: injectWhy().some(reason => reason.type === 'cache invalidated') ) { refs.current.counter++ } // setting a ref during evaluation is perfectly fine in Zedux refs.current.promise = injectMemo(() => { const prevController = refs.current.controller const nextController = typeof AbortController !== 'undefined' ? new AbortController() : undefined refs.current.controller = nextController const promise = promiseFactory(refs.current.controller) if (DEV && typeof promise?.then !== 'function') { throw new TypeError( `Zedux: injectPromise expected callback to return a promise. Received ${detailedTypeof( promise )}` ) } if (promise === refs.current.promise) return refs.current.promise if (prevController) (prevController as any).abort('updated') if (!dataOnly) { // preserve previous data and error using setStateDeep: store.setStateDeep( state => getInitialPromiseState( (state as PromiseState).data ) as RecursivePartial> ) } promise .then(data => { if (nextController?.signal.aborted) return store.setState(dataOnly ? data : getSuccessPromiseState(data)) }) .catch(error => { if (dataOnly || nextController?.signal.aborted) return // preserve previous data using setStateDeep: store.setStateDeep(getErrorPromiseState(error)) }) return promise }, deps && [...deps, refs.current.counter]) injectEffect( () => () => { const controller = refs.current.controller if (controller) (controller as any).abort('destroyed') }, [] ) return api(store).setPromise(refs.current.promise) }