Skip to content

Instantly share code, notes, and snippets.

@matiasfha
Created October 7, 2021 19:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matiasfha/f4c1f2c1c8f15a38b86a4fa74feaf8eb to your computer and use it in GitHub Desktop.
Save matiasfha/f4c1f2c1c8f15a38b86a4fa74feaf8eb to your computer and use it in GitHub Desktop.
import { toast, ToastTypes } from 'components/Toast';
import {
useCancel,
useStart
} from 'hooks';
import {
ActionFunctionMap,
ActionObject,
assign,
ConditionPredicate,
createMachine,
InvokeConfig,
MachineConfig,
ServiceConfig,
StateNodeConfig,
} from 'xstate';
import { Context, Events, Statuses } from './types';
const getProcessingState = (): StateNodeConfig<Context, any, Events, ActionObject<Context, Events>> => {
const invoke: InvokeConfig<Context, Events> = {
src: 'processing',
onDone: {
target: '#status.unknown', //After processing/mutating the state with RQ jump into the transient state
actions: assign({
job: (_, event) => {
return event.data;
},
}),
},
onError: {
target: '#status.failed',
actions: assign({
error: (_, event) => {
return event.data.response.errors[0].message;
},
}),
},
};
return {
invoke,
};
};
// A hierarchical machine to the cancellation state and actions
const getCancelledMachine = ({ prevState }: { prevState: string }) => {
return {
setCancelled: getProcessingState(),
cancelModalShow: {
on: {
TOCANCELLED: 'setCancelled',
BACK: prevState,
},
},
};
};
const preProdMachine: MachineConfig<Context, any, Events> = {
id: 'preProdMachine',
initial: 'preProd',
states: {
preProd: {
on: {
GET_DATE: 'getDate',
TOCANCELLED: 'cancelModalShow',
},
},
getDate: {
on: {
BACK: 'preProd',
TOINPROGRESS: 'setStartDate',
},
},
setStartDate: getProcessingState(),
...getCancelledMachine({ prevState: 'preProd' }),
},
};
export const getMachineGuards = (): Record<string, ConditionPredicate<Context, Events>> => {
return {
isQueued: (context) => context.job?.status === Statuses.QUEUE,
isCanceled: (context) => context.job?.status === Statuses.CANCELLED,
isPreprod: (context) => {
return context.job?.status === Statuses.PREPROD;
},
isInProgress: (context) => context.job?.status === Statuses.PROGRESS,
};
};
export const getMachineServices = (id: string): Record<string, ServiceConfig<Context, Events>> => {
const { mutateAsync: cancel } = useCancel(id);
const { mutateAsync: start } = useStart(id);
const services = {
processing: (context: Context, event: Events) => {
if (event.type === 'TOCANCELLED') {
return cancel({ jobId: context.entity.id });
}
if (event.type === 'TOINPROGRESS') {
return start({ jobId: context.entity.id, startDate: event.date });
}
return Promise.reject('Not implemented');
},
};
return services;
};
export const getStatusMachine = (entity: Entity): MachineConfig<Context, any, Events> => ({
id: 'status',
initial: 'unknown',
context: {
entity,
error: null,
},
/* This is a "global" event, meaning that, no matter in what state the machine is, the UPDATE event can be trigger
every time this event is sent, it will update the machine state to `unknown` target and will update the context through the action*/
on: {
UPDATE: {
target: 'unknown',
actions: ['updateContext'],
},
},
states: {
/* This is the default state, at start (or refresh) the machine don't know in what state start
it depends on the Job entity, so this state will `always` move to another state immediately based on the `guards` described
*/
unknown: {
always: [
{ target: Statuses.CANCELLED, cond: 'isCanceled' },
{ target: Statuses.PREPROD, cond: 'isPreprod' },
{ target: Statuses.PROGRESS, cond: 'isInProgress' },
],
},
/**Triggering promises is an state it self */
failed: {
entry: ['errorNotification'], // trigger an action on entry
},
[Statuses.PREPROD]: {
...preProdMachine,
},
[Statuses.CANCELLED]: {
type: 'final',
},
[Statuses.COMPLETE]: {
type: 'final',
},
},
});
export const getMachineActions = (): ActionFunctionMap<Context, Events, ActionObject<Context, Events>> => {
return {
errorNotification: (context) => {
alert(context.error as string);
},
updateContext: assign({
entity: (_, event) => {
if (event.type === 'UPDATE') {
return event.entity;
}
},
}),
};
};
export const getCreatedMachine = ({
job,
...rest
}: {
job: Entity;
services?: Record<string, ServiceConfig<Context, Events>>;
guards?: Record<string, ConditionPredicate<Context, Events>>;
actions?: ActionFunctionMap<Context, Events, ActionObject<Context, Events>>;
}) => {
const {
services = getMachineServices(entity.id),
guards = getMachineGuards(),
actions = getMachineActions(),
} = rest;
const machine = createMachine<Context, Events>(getStatusMachine(job), {
actions,
guards,
services,
}).withContext({ entity, error: null });
return machine;
};
import * as React from 'react';
import { send } from 'xstate/lib/actionTypes';
import { Info } from './components.styled';
import { StatusMachineContext } from './StatusMachineProvider';
import { useSelectors } from './useSelectors';
export const InfoComponent = () => {
const { isQueue, isInProgress, isCancelled, isBid } = useSelectors();
const { statusService } = React.useContext(StatusMachineContext);
const { send } = statusService;
if (isQueue) {
return (
<Info>
<h2>Status Info</h2>
<h1>Set Start Date</h1>
</Info>
);
}
if (isInProgress) {
return (
<Info>
<h2>Status Info</h2>
<h1>In progress</h1>
<button onClick={() => send('TOCOMPLETE')}>Complete</button> </Info>
);
}
if (isCancelled) {
return (
<Info>
<h2>Status Info</h2>
<h1>Cancelled</h1>
</Info>
);
}
return null;
};
import { assign, createMachine, Interpreter } from 'xstate';
import { useInterpret } from '@xstate/react';
import { getCreatedMachine } from './machineConfig';
import { useLayoutEffect } from 'react';
import { Context, Events, Statuses, Entity } from './types';
import React from 'react';
type StatusService = Interpreter<Context, any, Events, { value: any; context: Context }>;
type ContextMachine = {
statuses: Statuses[];
statusService: StatusService;
};
export const StatusMachineContext = React.createContext<ContextMachine>({
statuses: [],
statusService: {} as StatusService,
});
export const StatusMachineProvider = ({ entity, children }: { entity: Entity | undefined; children: React.ReactNode }) => {
const statusService = useInterpret(getCreatedMachine({ job }));
// Refresh machine context when the data change because of RQ
useLayoutEffect(() => {
if (entity && !statusService.state.done) {
statusService.send('UPDATE', { entity });
}
}, [entity?.status]);
if (!statusService.initialized && !statusService.state?.done) {
return null;
}
const statuses = getStatusesPath(true);
return (
<StatusMachineContext.Provider value={{ statusService, statuses }}>{children}</StatusMachineContext.Provider>
);
};
import { useActor, useSelector } from '@xstate/react';
import { Context, Events, Statuses } from './types';
import { EventObject, State, Typestate } from 'xstate';
import React from 'react';
import { StatusMachineContext } from './StatusMachineProvider';
type StatusState = State<Context, EventObject, Typestate<Context>>;
const preprodSelector = (state: StatusState) => state.matches(Statuses.PREPROD);
const inProgressSelector = (state: StatusState) => state.matches(Statuses.PROGRESS);
const cancelledSelector = (state: StatusState) => state.matches(Statuses.CANCELLED);
const preProdGetDateSelector = (state: StatusState) => state.matches({ [Statuses.PREPROD]: 'getDate' });
const progressGetDateSelector = (state: StatusState) => state.matches({ [Statuses.PROGRESS]: 'getDate' });
const cancelledModalSelector = (state: StatusState) => {
const inProgress = state.matches({ [Statuses.PROGRESS]: 'cancelModalShow' });
const inQueue = state.matches({ [Statuses.QUEUE]: 'cancelModalShow' });
const inPreprod = state.matches({ [Statuses.PREPROD]: 'cancelModalShow' });
return inProgress || inQueue || inPreprod;
};
export const useSelectors = () => {
const { statusService } = React.useContext(StatusMachineContext);
const isPreprod = useSelector(statusService, preprodSelector);
const isInProgress = useSelector(statusService, inProgressSelector);
const isProgressGetDate = useSelector(statusService, progressGetDateSelector);
const isCancelled = useSelector(statusService, cancelledSelector);
const isCancelledModalShow = useSelector(statusService, cancelledModalSelector);
return {
isPreprod,
isInProgress,
isProgressGetDate,
isCancelledModalShow,
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment