Last active
September 15, 2021 23:10
-
-
Save jenya239/c15b0122590598109905e4f282592539 to your computer and use it in GitHub Desktop.
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 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