Thoughts on standardizing REST with machines
Last active
September 29, 2021 12:28
-
-
Save hew/b3ed0736583556da9662f2f88bcea487 to your computer and use it in GitHub Desktop.
Thoughts on standardizing REST with machines
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 { useMachine } from '@xstate/react' | |
import { useAtom } from 'jotai' | |
import { AnyEventObject, interpret } from 'xstate' | |
import { createModel } from 'xstate/lib/model' | |
import { handle, extraHeaders } from '../../utils/http' | |
import { sessionAtom } from '../../utils/state' | |
import { toast } from 'react-toastify' | |
interface Context<Payload, Response> { | |
session: Session.shape | |
endpoint: string | |
method: string | |
reusable?: boolean | |
errorMessage?: string | |
inputValues?: Payload | |
createResult?: Response | |
updateResult?: Response | |
insertResult?: Response | |
getResult?: Response | |
} | |
interface HookParams { | |
name?: string | |
endpoint: string | |
reusable?: boolean | |
v2?: boolean | |
} | |
interface CallFetch { | |
session: Session.shape | |
endpoint: string | |
type?: string | |
data?: any | |
} | |
interface MachineParams extends HookParams { | |
session: Session.shape | |
} | |
export const createFetchMachine = <Payload, Response>({ | |
name, | |
session, | |
reusable, | |
endpoint, | |
v2, | |
}: MachineParams) => { | |
const context: Context<Payload, Response> = { | |
session, | |
endpoint, | |
reusable, | |
method: 'get', | |
} | |
const Model = createModel(context, { | |
events: { | |
get: () => ({}), | |
create: (data: Payload) => ({ data }), | |
update: (data: Payload) => ({ data }), | |
insert: (data: Payload) => ({ data }), | |
delete: () => ({}), | |
}, | |
}) | |
const assignFetch = Model.assign({ method: 'get' }) | |
const assignDelete = Model.assign({ method: 'delete' }) | |
const assignPost = Model.assign({ method: 'post', inputValues: (_, evt) => evt.data }, 'create') | |
const assignPatch = Model.assign({ method: 'patch', inputValues: (_, evt) => evt.data }, 'update') | |
const assignPut = Model.assign({ method: 'put', inputValues: (_, evt) => evt.data }, 'insert') | |
const assignError = Model.assign({ errorMessage: (_, event: AnyEventObject) => event.data.message }) | |
const assignGet = Model.assign({ getResult: (_, event: AnyEventObject) => event.data }) | |
const assignCreate = Model.assign({ createResult: (_, event: AnyEventObject) => event.data }) | |
const assignUpdate = Model.assign({ updateResult: (_, event: AnyEventObject) => event.data }) | |
const assignInsert = Model.assign({ insertResult: (_, event: AnyEventObject) => event.data }) | |
const errorEntryAction = (ctx: Context<Payload, Response>) => { | |
console.error(ctx.errorMessage) | |
toast.error(ctx.errorMessage) | |
} | |
const isReusable = (ctx: Context<Payload, Response>) => ctx.reusable | |
async function networkCall(ctx: Context<Payload, Response>) { | |
const path: string = `${v2 ? session.routes.adminAPIUrl2 : session.routes.adminAPIUrl}/${ctx.endpoint}` | |
const args: RequestInit = { | |
method: ctx.method, | |
body: ctx.method === 'post' || ctx.method === 'put' ? JSON.stringify(ctx.inputValues) : undefined, | |
headers: { token: ctx.session.tokens.dealerToken, ...extraHeaders }, | |
} | |
return handle(await fetch(path, args)) | |
} | |
return Model.createMachine( | |
{ | |
id: name, | |
initial: 'idle', | |
context: Model.initialContext, | |
states: { | |
idle: { | |
on: { | |
get: 'fetching', | |
create: 'creating', | |
update: 'updating', | |
insert: 'inserting', | |
delete: 'deleting', | |
}, | |
}, | |
fetching: { | |
entry: assignFetch, | |
// @ts-ignore | |
invoke: { | |
src: 'networkCall', | |
onDone: [ | |
{ | |
cond: isReusable, | |
target: 'idle', | |
actions: assignGet, | |
}, | |
{ | |
target: 'complete', | |
actions: assignGet, | |
}, | |
], | |
onError: { | |
actions: assignError, | |
target: 'error', | |
}, | |
}, | |
}, | |
creating: { | |
// @ts-ignore | |
entry: assignPost, | |
// @ts-ignore | |
invoke: { | |
src: 'networkCall', | |
onDone: [ | |
{ | |
cond: isReusable, | |
target: 'idle', | |
actions: assignCreate, | |
}, | |
{ | |
target: 'complete', | |
actions: assignCreate, | |
}, | |
], | |
onError: { | |
actions: assignError, | |
target: 'error', | |
}, | |
}, | |
}, | |
updating: { | |
// @ts-ignore | |
entry: assignPatch, | |
// @ts-ignore | |
invoke: { | |
src: 'networkCall', | |
onDone: [ | |
{ | |
cond: isReusable, | |
target: 'idle', | |
actions: assignUpdate, | |
}, | |
{ | |
target: 'complete', | |
actions: assignUpdate, | |
}, | |
], | |
onError: { | |
actions: assignError, | |
target: 'error', | |
}, | |
}, | |
}, | |
inserting: { | |
// @ts-ignore | |
entry: assignPut, | |
// @ts-ignore | |
invoke: { | |
src: 'networkCall', | |
onDone: [ | |
{ | |
cond: isReusable, | |
target: 'idle', | |
actions: assignInsert, | |
}, | |
{ | |
target: 'complete', | |
actions: assignInsert, | |
}, | |
], | |
onError: { | |
actions: assignError, | |
target: 'error', | |
}, | |
}, | |
}, | |
deleting: { | |
entry: assignDelete, | |
// @ts-ignore | |
invoke: { | |
src: 'networkCall', | |
onDone: [ | |
{ | |
cond: isReusable, | |
target: 'idle', | |
}, | |
{ | |
target: 'complete', | |
}, | |
], | |
onError: { | |
actions: assignError, | |
target: 'error', | |
}, | |
}, | |
}, | |
complete: { type: 'final' }, | |
error: { | |
entry: errorEntryAction, | |
after: { | |
1000: 'idle', | |
}, | |
}, | |
}, | |
}, | |
{ | |
services: { | |
networkCall, | |
}, | |
} | |
) | |
} | |
export const useFetch = <Payload, Response>({ | |
name = 'fetch' + Math.random(), | |
endpoint, | |
reusable, | |
v2, | |
}: HookParams) => { | |
const [session]: [Session.shape, any] = useAtom(sessionAtom) | |
return useMachine(createFetchMachine<Payload, Response>({ session, endpoint, reusable, name, v2 }), { | |
devTools: true, | |
}) | |
} | |
export const callFetch = <Payload, Response>({ | |
session, | |
endpoint, | |
data, | |
type = 'get', | |
}: CallFetch): Promise<Response> => { | |
return new Promise((resolve) => { | |
const fetchMachine = createFetchMachine<Payload, Response>({ session, endpoint }) | |
const service = interpret(fetchMachine).onTransition((state) => { | |
if (state.value === 'complete') { | |
const result: Response = state.context[`${type}Result`] | |
resolve(result) | |
service.stop() | |
} | |
}) | |
service.start() | |
// @ts-ignore | |
data ? service.send({ type, data }) : service.send({ type }) | |
}) | |
} |
What if you want to make more than one request at once?
I’d say the use case for this isn’t multiple requests. More like a component in a CMS system.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could also do interface
Context<Get, Create, Update> {}