Created
April 22, 2025 12:10
-
-
Save mobily/c7c9c93cefe3905c4bf5884fab2d07c7 to your computer and use it in GitHub Desktop.
AsyncData
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { Array, Data, Match, Option } from 'effect'; | |
| import { constant, dual, identity, pipe } from 'effect/Function'; | |
| export type AsyncData<T> = Data.TaggedEnum<{ | |
| Init: object; | |
| Loading: object; | |
| Reloading: { | |
| data: T; | |
| }; | |
| Complete: { | |
| data: T; | |
| }; | |
| }>; | |
| type Extract<A, T extends AsyncData<A>['_tag']> = Data.TaggedEnum.Value<AsyncData<A>, T>; | |
| type Init<T> = Extract<T, 'Init'>; | |
| type Loading<T> = Extract<T, 'Loading'>; | |
| type Reloading<T> = Extract<T, 'Reloading'>; | |
| type Complete<T> = Extract<T, 'Complete'>; | |
| type TypeOfAsyncData<T> = T extends AsyncData<infer U> ? U : never; | |
| type TypeOfAsyncDataArray<T extends readonly unknown[]> = T extends [infer Head, ...infer Tail] | |
| ? readonly [TypeOfAsyncData<Head>, ...TypeOfAsyncDataArray<Tail>] | |
| : readonly []; | |
| interface AsyncDataTaggedEnum extends Data.TaggedEnum.WithGenerics<1> { | |
| readonly taggedEnum: AsyncData<this['A']>; | |
| } | |
| const AsyncData = Data.taggedEnum<AsyncDataTaggedEnum>(); | |
| export const Init = <T>(): AsyncData<T> => { | |
| return AsyncData.Init<T>(); | |
| }; | |
| export const Loading = <T>(): AsyncData<T> => { | |
| return AsyncData.Loading<T>(); | |
| }; | |
| export const Reloading = <T>(data: T): AsyncData<T> => { | |
| return AsyncData.Reloading({ data }); | |
| }; | |
| export const Complete = <T>(data: T): AsyncData<T> => { | |
| return AsyncData.Complete({ data }); | |
| }; | |
| export const isInit = <T>(asyncData: AsyncData<T>): asyncData is Init<T> => { | |
| return asyncData._tag === 'Init'; | |
| }; | |
| export const isLoading = <T>(asyncData: AsyncData<T>): asyncData is Loading<T> => { | |
| return asyncData._tag === 'Loading'; | |
| }; | |
| export const isReloading = <T>(asyncData: AsyncData<T>): asyncData is Reloading<T> => { | |
| return asyncData._tag === 'Reloading'; | |
| }; | |
| export const isComplete = <T>(asyncData: AsyncData<T>): asyncData is Complete<T> => { | |
| return asyncData._tag === 'Complete'; | |
| }; | |
| export const isBusy = <T>(asyncData: AsyncData<T>): asyncData is Loading<T> | Reloading<T> => { | |
| return isLoading(asyncData) || isReloading(asyncData); | |
| }; | |
| export const isIdle = <T>(asyncData: AsyncData<T>): asyncData is Init<T> | Complete<T> => { | |
| return !isBusy(asyncData); | |
| }; | |
| export const isEmpty = <T>(asyncData: AsyncData<T>): asyncData is Init<T> | Loading<T> => { | |
| return isInit(asyncData) || isLoading(asyncData); | |
| }; | |
| export const isNotEmpty = <T>(asyncData: AsyncData<T>): asyncData is Complete<T> | Reloading<T> => { | |
| return !isEmpty(asyncData); | |
| }; | |
| export const getWithDefault: { | |
| <T>(defaultValue: T): (asyncData: AsyncData<T>) => T; | |
| <T>(asyncData: AsyncData<T>, defaultValue: T): T; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, defaultValue: T) => { | |
| if (isEmpty(asyncData)) { | |
| return defaultValue; | |
| } | |
| return asyncData.data; | |
| }); | |
| export const map: { | |
| <T, R>(mapFn: (value: T) => R): (asyncData: AsyncData<T>) => AsyncData<R>; | |
| <T, R>(asyncData: AsyncData<T>, mapFn: (value: T) => R): AsyncData<R>; | |
| } = dual(2, <T, R>(asyncData: AsyncData<T>, mapFn: (value: T) => R) => { | |
| const matcher = Match.type<AsyncData<T>>().pipe( | |
| Match.tag('Init', constant(asyncData)), | |
| Match.tag('Loading', constant(asyncData)), | |
| Match.tag('Reloading', value => { | |
| return Reloading(mapFn(value.data)); | |
| }), | |
| Match.tag('Complete', value => { | |
| return Complete(mapFn(value.data)); | |
| }), | |
| Match.exhaustive, | |
| ); | |
| return matcher(asyncData); | |
| }); | |
| export const flatMap: { | |
| <T, R>(mapFn: (value: T) => AsyncData<R>): (asyncData: AsyncData<T>) => AsyncData<R>; | |
| <T, R>(asyncData: AsyncData<T>, mapFn: (value: T) => AsyncData<R>): AsyncData<R>; | |
| } = dual(2, <T, R>(asyncData: AsyncData<T>, mapFn: (value: T) => AsyncData<R>) => { | |
| const matcher = Match.type<AsyncData<T>>().pipe( | |
| Match.tag('Init', identity), | |
| Match.tag('Loading', identity), | |
| Match.tag('Reloading', value => { | |
| return mapFn(value.data); | |
| }), | |
| Match.tag('Complete', value => { | |
| return mapFn(value.data); | |
| }), | |
| Match.exhaustive, | |
| ); | |
| return matcher(asyncData); | |
| }); | |
| export const all = <T extends readonly [...AsyncData<any>[]]>( | |
| array: [...T], | |
| ): AsyncData<TypeOfAsyncDataArray<T>> => { | |
| return pipe( | |
| array, | |
| Array.reduce(Complete([]) as AsyncData<TypeOfAsyncDataArray<T>>, (acc, prev) => { | |
| return flatMap(acc, values => { | |
| const matcher = Match.type<AsyncData<unknown[]>>().pipe( | |
| Match.tag('Init', identity), | |
| Match.tag('Loading', identity), | |
| Match.tag('Reloading', value => { | |
| return Reloading(Array.append(values, value.data)); | |
| }), | |
| Match.tag('Complete', value => { | |
| return Complete(Array.append(values, value.data)); | |
| }), | |
| Match.exhaustive, | |
| ); | |
| return matcher(prev) as AsyncData<TypeOfAsyncDataArray<T>>; | |
| }); | |
| }), | |
| asyncData => { | |
| if (isComplete(asyncData)) { | |
| const isAnyReloading = Array.some(asyncData.data, isReloading); | |
| return isAnyReloading ? Reloading(asyncData.data) : Complete(asyncData.data); | |
| } | |
| return asyncData; | |
| }, | |
| ); | |
| }; | |
| export const getValue = <T>(asyncData: AsyncData<T>): Option.Option<T> => { | |
| return isNotEmpty(asyncData) ? Option.some(asyncData.data) : Option.none(); | |
| }; | |
| export const getComplete = <T>(asyncData: AsyncData<T>): Option.Option<T> => { | |
| return isComplete(asyncData) ? Option.some(asyncData.data) : Option.none(); | |
| }; | |
| export const getReloading = <T>(asyncData: AsyncData<T>): Option.Option<T> => { | |
| return isReloading(asyncData) ? Option.some(asyncData.data) : Option.none(); | |
| }; | |
| export const getOrThrow = <T>(asyncData: AsyncData<T>): T => { | |
| return Option.getOrThrow(getValue(asyncData)); | |
| }; | |
| export const tapInit: { | |
| <T>(tapFn: () => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: () => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: () => void) => { | |
| if (isInit(asyncData)) { | |
| tapFn(); | |
| } | |
| return asyncData; | |
| }); | |
| export const tapLoading: { | |
| <T>(tapFn: () => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: () => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: () => void) => { | |
| if (isLoading(asyncData)) { | |
| tapFn(); | |
| } | |
| return asyncData; | |
| }); | |
| export const tapEmpty: { | |
| <T>(tapFn: () => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: () => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: () => void) => { | |
| if (isEmpty(asyncData)) { | |
| tapFn(); | |
| } | |
| return asyncData; | |
| }); | |
| export const tapReloading: { | |
| <T>(tapFn: (value: T) => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void) => { | |
| if (isReloading(asyncData)) { | |
| tapFn(asyncData.data); | |
| } | |
| return asyncData; | |
| }); | |
| export const tapComplete: { | |
| <T>(tapFn: (value: T) => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void) => { | |
| if (isComplete(asyncData)) { | |
| tapFn(asyncData.data); | |
| } | |
| return asyncData; | |
| }); | |
| export const tapNotEmpty: { | |
| <T>(tapFn: (value: T) => void): (asyncData: AsyncData<T>) => AsyncData<T>; | |
| <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void): AsyncData<T>; | |
| } = dual(2, <T>(asyncData: AsyncData<T>, tapFn: (value: T) => void) => { | |
| if (isNotEmpty(asyncData)) { | |
| tapFn(asyncData.data); | |
| } | |
| return asyncData; | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment