Skip to content

Instantly share code, notes, and snippets.

@jenya239
Last active September 15, 2021 23:10
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 jenya239/c15b0122590598109905e4f282592539 to your computer and use it in GitHub Desktop.
Save jenya239/c15b0122590598109905e4f282592539 to your computer and use it in GitHub Desktop.
import React, { Context, Reducer, useContext, useEffect, useMemo, useReducer } from 'react'
type Status = 'init' | 'processing' | 'error' | 'success'
interface IItemState<Item> {
id: number
stage: number
item: Item
status: Status
}
interface IBatch<Item> {
id: number
itemStates: readonly IItemState<Item>[]
processors: ((item: Item) => Promise<void>)[]
}
type Action<Item> =
| { type: 'add'; batch: IBatch<Item> }
| { type: 'remove'; batch: IBatch<Item> }
| { type: 'replace'; batch: IBatch<Item> }
| { type: 'start'; batch: IBatch<Item>; itemState: IItemState<Item> }
| { type: 'failure'; batch: IBatch<Item>; itemState: IItemState<Item> }
| { type: 'success'; batch: IBatch<Item>; itemState: IItemState<Item> }
| { type: 'next'; batch: IBatch<Item>; itemState: IItemState<Item> }
const reducer = <Item extends unknown>(
batches: readonly IBatch<Item>[],
action: Action<Item>
): readonly IBatch<Item>[] => {
const replaceBatch = (batch: IBatch<Item>) =>
Object.freeze(batches.map((b) => (b.id === batch.id ? batch : b)))
const replaceItemState = (batch: IBatch<Item>, is: IItemState<Item>) =>
Object.freeze(
batches.map((b) =>
b.id !== batch.id
? b
: Object.freeze({
...b,
itemStates: Object.freeze(b.itemStates.map((bis) => (bis.id === is.id ? is : bis))),
})
)
)
const changeStatus = (is: IItemState<Item>, status: Status) =>
replaceItemState(action.batch, Object.freeze({ ...is, status }))
switch (action.type) {
case 'add':
return Object.freeze([...batches, Object.freeze(action.batch)])
case 'remove':
return Object.freeze(batches.filter((b) => b.id !== action.batch.id))
case 'replace':
return replaceBatch(action.batch)
case 'start':
return changeStatus(action.itemState, 'processing')
case 'failure':
return changeStatus(action.itemState, 'error')
case 'success':
return changeStatus(action.itemState, 'success')
case 'next':
return replaceItemState(
action.batch,
Object.freeze({ ...action.itemState, status: 'init', stage: action.itemState.stage + 1 })
)
}
}
export const useBatchProcessingReducer = <Item extends unknown>(): [
readonly IBatch<Item>[],
(action: Action<Item>) => void
] => useReducer<Reducer<readonly IBatch<Item>[], Action<Item>>>(reducer, [])
interface IActions<Item> {
start: (items: Item[]) => number
}
type FullState<Item> = { batches: readonly IBatch<Item>[] } & IActions<Item>
let idCounter = 0
export const createContext = <Item extends unknown>(): Context<FullState<Item> | undefined> =>
React.createContext<FullState<Item> | undefined>(undefined)
interface IBatchProviderProps<Item> {
context: Context<FullState<Item> | undefined>
batches: readonly IBatch<Item>[]
dispatch: (action: Action<Item>) => void
processors: ((item: Item) => Promise<void>)[]
children: React.ReactNode
completeHandler?: () => void
}
export const BatchProcessingProvider = <Item extends unknown>({
context,
batches,
dispatch,
processors,
children,
completeHandler,
}: IBatchProviderProps<Item>): JSX.Element => {
const actions = useMemo<IActions<Item>>(
() => ({
start: (items: Item[]) => {
const id = idCounter++
dispatch({
type: 'add',
batch: {
id,
itemStates: Object.freeze(
items.map((item) =>
Object.freeze({
id: idCounter++,
stage: -1,
item,
status: 'init',
})
)
),
processors,
},
})
return id
},
}),
[processors]
)
useEffect(() => {
for (const batch of batches) {
let allDone = true
for (const is of batch.itemStates) {
if (is.stage < batch.processors.length) {
allDone = false
if (is.stage < 0) {
dispatch({ type: 'next', batch, itemState: is })
} else {
if (is.status === 'init') {
dispatch({ type: 'start', batch, itemState: is })
batch.processors[is.stage](is.item)
.then(() => dispatch({ type: 'success', batch, itemState: is }))
.catch(() => dispatch({ type: 'failure', batch, itemState: is }))
} else if (is.status !== 'processing') {
dispatch({ type: 'next', batch, itemState: is })
}
}
}
}
if (allDone) {
dispatch({ type: 'remove', batch })
completeHandler && completeHandler()
}
}
}, [batches])
return (
<context.Provider value={Object.freeze({ batches, ...actions })}>{children}</context.Provider>
)
}
export const useBatchProcessing = <Item extends unknown>(
Context: Context<FullState<Item> | undefined>,
name: string
): FullState<Item> => {
const context = useContext(Context)
if (context === undefined) {
throw new Error(`useContext must be used within a ${name}ProcessingProvider`)
}
return context
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment