Skip to content

Instantly share code, notes, and snippets.

@mobily
Created April 22, 2025 12:10
Show Gist options
  • Select an option

  • Save mobily/c7c9c93cefe3905c4bf5884fab2d07c7 to your computer and use it in GitHub Desktop.

Select an option

Save mobily/c7c9c93cefe3905c4bf5884fab2d07c7 to your computer and use it in GitHub Desktop.
AsyncData
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