Skip to content

Instantly share code, notes, and snippets.

@hew
Last active September 29, 2021 12:28
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save hew/b3ed0736583556da9662f2f88bcea487 to your computer and use it in GitHub Desktop.
Save hew/b3ed0736583556da9662f2f88bcea487 to your computer and use it in GitHub Desktop.
Thoughts on standardizing REST with machines

Thoughts on standardizing REST with machines

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 })
})
}
@hew
Copy link
Author

hew commented Aug 23, 2021

Could also do interface Context<Get, Create, Update> {}

@ChrisShank
Copy link

What if you want to make more than one request at once?

@hew
Copy link
Author

hew commented Aug 25, 2021

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