Skip to content

Instantly share code, notes, and snippets.

@UberMouse
Last active July 20, 2022 13:24
Show Gist options
  • Save UberMouse/66bedabd1ca881bef38de7e7893c98c4 to your computer and use it in GitHub Desktop.
Save UberMouse/66bedabd1ca881bef38de7e7893c98c4 to your computer and use it in GitHub Desktop.
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { Required } from "utility-types";
import { createMachine, sendParent, assign, spawn, Actor } from "xstate";
import { $YesReallyAny, assert } from "../";
import { QueryResult } from "../gql/queryResultHandler";
let id = 0;
enum Actions {
submitData = "submitData",
updateParameters = "updateParameters",
spawnObservable = "spawnObservable",
}
type ParentEvents<T> =
| { type: "dataLoader.DATA_LOADED"; data: T }
| { type: "dataLoader.FAILED_TO_LOAD_DATA" };
export function createDataLoader<
TObservableFactory extends (...args: $YesReallyAny[]) => Observable<QueryResult<$YesReallyAny>>,
TData = ReturnType<TObservableFactory> extends Observable<QueryResult<infer TResult>>
? TResult
: never,
TParameters = Parameters<TObservableFactory>
>(observableFactory: TObservableFactory) {
type ObservableEvents = { type: "$NEW_DATA"; data: TData } | { type: "$ERROR" };
type Events = { type: "FETCH"; params: TParameters } | ObservableEvents;
type Context = {
params: TParameters;
fetchImmediately: boolean;
observable?: Actor<$YesReallyAny, ObservableEvents>;
};
type SpawnedContext = Required<Context, "observable">;
type State =
| { value: "loading"; context: SpawnedContext }
| { value: "error"; context: SpawnedContext }
| { value: "loaded"; context: SpawnedContext }
| { value: "waitingToFetch"; context: Context };
return createMachine<Context, Events, State>(
{
id: `data-loader-${id++}`,
initial: "fetchOrNot",
context: { params: [] as $YesReallyAny, fetchImmediately: true },
on: {
FETCH: { target: "loading", actions: Actions.updateParameters },
$ERROR: "error",
$NEW_DATA: { target: "loaded", actions: Actions.submitData },
},
states: {
fetchOrNot: {
on: {
"": [
{ target: "loading", cond: (ctx) => ctx.fetchImmediately },
{ target: "waitingToFetch" },
],
},
},
waitingToFetch: {},
loading: {
entry: Actions.spawnObservable,
},
error: {
entry: sendParent<Context, Events, ParentEvents<TData>>("dataLoader.FAILED_TO_LOAD_DATA"),
},
loaded: {},
},
},
{
actions: {
[Actions.submitData]: sendParent<Context, Events, ParentEvents<TData>>((_ctx, event) => {
assert(event.type === "$NEW_DATA");
return { type: "dataLoader.DATA_LOADED", data: event.data };
}),
[Actions.updateParameters]: assign({
params: (_ctx, e) => {
assert(e.type === "FETCH");
return e.params;
},
}),
[Actions.spawnObservable]: assign({
observable: ({ params }) => {
return spawn(
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// @ts-ignore
observableFactory(...params).pipe(
map(
(x: QueryResult<TData>): ObservableEvents => {
if (x.type === "error") {
return { type: "$ERROR" };
} else {
return { type: "$NEW_DATA", data: x.data };
}
}
)
)
);
},
}),
},
}
);
}
export type { ParentEvents as DataLoaderEvents };
gql`
query getRepositories {
repositories {
id
name
path
branchInfo {
branches {
commit
abbrevCommit
name
}
current
}
}
}
`;
export function getRepositories(): Observable<QueryResult<Repository[]>> {
const query = gqlClient.watchQuery<GetRepositoriesQuery, GetRepositoriesQueryVariables>({
query: GetRepositoriesDocument,
});
return handleQueryResult(query, (query) =>
query.repositories.map((repo) => ({
id: repo.id,
name: repo.name,
path: repo.path,
branchInfo: repo.branchInfo,
}))
);
}
export type QueryResult<TTransformedData> =
| { type: "success"; data: TTransformedData }
| { type: "error"; errors: readonly GraphQLError[] };
export function handleQueryResult<TQueryType, TVariables, TResultType>(
query: ObservableQuery<TQueryType, TVariables>,
mapper: (queryResult: TQueryType) => TResultType
): Observable<QueryResult<TResultType>> {
const rxjsResults = new Observable<QueryResult<TResultType>>((observer) => {
query.subscribe((result) => {
if (result.errors) {
observer.next({ type: "error", errors: result.errors });
} else if (!result.loading) {
observer.next({ type: "success", data: mapper(result.data) });
}
});
});
return rxjsResults;
}
const dataLoader = createDataLoader(getRepositories);
const machine = createMachine(
{
id: "repository-list",
initial: "fetching",
context: {},
on: {
"dataLoader.DATA_LOADED": [
{
target: "#hasRepositories",
actions: [Actions.storeLoadedRepos, Actions.markFirstRepoAsActive],
cond: (_ctx, e) => e.data.length > 0,
},
{
target: "#noRepositories",
},
],
"dataLoader.FAILED_TO_LOAD_DATA": "fetching.error",
},
states: {
fetching: {
entry: assign({
loader: () => spawn(dataLoader, "dataLoader"),
}),
initial: "loading",
states: {
loading: {},
error: {},
},
},
loaded: {
},
},
},
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment