@excentone/spfx-react
Version:
Contains custom ReactJs components and hooks intended to use when developing SharePoint Framework (SPFx) Web components.
312 lines (310 loc) • 11.5 kB
JavaScript
import { useCounter } from "../useCounter";
import { assertNever } from "@fluentui/react";
import { isEqual } from "@microsoft/sp-lodash-subset";
import { isFunction } from "@excentone/spfx-utilities";
import { useEffect, useMemo, useReducer, useState } from "react";
const initialIndex = -1;
const reducer = (prev, action) => {
const { type, index, jobs, setJob, status } = action;
const next = [...prev];
switch (type) {
case 'PREPEND':
next.splice(0, 0, ...jobs);
return next;
case 'APPEND':
next.push(...jobs);
return next;
case 'INSERT':
next.splice(index, 0, ...jobs);
return next;
case 'UPDATE':
if (0 <= index && index < next.length) {
const { handler, skip, ...details } = next[index];
const nextJob = isFunction(setJob)
? setJob(details)
: setJob;
if (!isEqual(details, nextJob)) {
next.splice(index, 1, { handler, skip, ...nextJob });
return next;
}
}
return prev;
case 'SET_STATUS':
return prev.map(job => {
job.status = status || 'Idle';
return job;
});
case 'SET':
return jobs
? jobs.map(job => {
job.status = 'Idle';
return job;
})
: [];
case 'CLEAR':
return [];
default:
assertNever(type);
}
};
export const usePipeline = (options, deps = []) => {
const { name, onJobStart, onJobSuccess, onJobFailed, onJobFinished, onPipelineStart, onPipelineFailed, onPipelineSuccess, onPipelineFinished, jobExecutionDelay, getErrorMessage, jobs: jobList, values: initialValues, } = useMemo(() => ({
...{
jobs: [],
values: {},
jobExecutionDelay: 500
},
...options
}), [options, ...(deps || [])]);
const [status, setStatus] = useState('Idle');
const [events, setEvents] = useState(null);
const [jobs, dispatch] = useReducer(reducer, []);
const [result, setResult] = useState({
values: null,
result: 'Unknown',
errors: []
});
const [currentIndex, { set, reset, increment }] = useCounter({
defaultValue: initialIndex,
initialValue: initialIndex
});
useEffect(() => {
if (status === 'Executing'
&& result.values && jobs.length
&& initialIndex < currentIndex
&& currentIndex < jobs.length) {
const job = jobs[currentIndex];
const { handler, skip, ...details } = job;
let jobDetails = {
...details,
status: 'Executing',
values: result.values
};
const updateResult = () => setResult(prev => {
let next = {
...prev,
values: jobDetails.values
};
if (currentIndex === jobs.length - 1) {
next = {
...next,
result: next.errors.length
? next.errors.every(e => e.jobDetails.ignoreOnError)
? 'Success with errors'
: 'Failed'
: 'Success'
};
}
else if (jobDetails.hasError && !jobDetails.ignoreOnError) {
next = {
...next,
result: 'Failed'
};
}
return next;
});
jobDetails.skipped = isFunction(skip)
? skip(result.values, jobDetails)
: skip;
if (jobDetails.skipped) {
dispatch({
type: 'UPDATE',
index: currentIndex,
setJob: prev => ({
...prev,
...jobDetails,
title: `${prev.title} - Skipped`,
status: 'Completed',
values: result.values
})
});
updateResult();
return;
}
if (onJobStart)
onJobStart(jobDetails, currentIndex);
if (events && events.onJobStart)
events.onJobStart(jobDetails, currentIndex);
handler(result.values, currentIndex, details, setJob => dispatch({ type: 'UPDATE', setJob, index: currentIndex }))
.then(values => {
jobDetails.values = values;
if (onJobSuccess)
onJobSuccess(jobDetails, currentIndex);
if (events && events.onJobSuccess)
events.onJobSuccess(jobDetails, currentIndex);
})
.catch(async (error) => {
jobDetails = {
...jobDetails,
hasError: true,
errorMessage: await getErrorMessage(error),
values: result.values
};
const pipelineError = {
values: result.values,
error,
jobDetails,
currentIndex
};
if (onJobFailed)
onJobFailed(pipelineError);
if (events && events.onJobFailed)
events.onJobFailed(pipelineError);
setResult(prev => {
const next = { ...prev };
next.errors.push(pipelineError);
return next;
});
})
.finally(() => {
if (onJobFinished)
onJobFinished(jobDetails, currentIndex);
if (events && events.onJobFinished)
events.onJobFinished(jobDetails, currentIndex);
dispatch({
type: 'UPDATE',
index: currentIndex,
setJob: prev => ({
...prev,
...jobDetails,
status: 'Completed',
})
});
updateResult();
});
}
}, [currentIndex]);
useEffect(() => {
if (status === 'Executing'
&& result.values && jobs.length
&& initialIndex < currentIndex
&& currentIndex < jobs.length) {
const job = jobs[currentIndex];
if (job.status === 'Completed' && job.values === result.values) {
const incrementOrSetMax = () => {
if (job.hasError && !job.ignoreOnError)
set(jobs.length);
else
increment();
};
if (jobExecutionDelay)
setTimeout(incrementOrSetMax, jobExecutionDelay);
else
incrementOrSetMax();
}
}
}, [jobs[currentIndex], result.values]);
useEffect(() => {
switch (status) {
case 'Idle':
break;
case 'Pending':
if (result.result === 'Unknown' && result.values) {
const initialState = {
currentIndex: currentIndex + 1,
jobs: jobs,
status,
name,
};
if (onPipelineStart)
onPipelineStart(result.values, initialState);
if (events && events.onPipelineStart)
events.onPipelineStart(result.values, initialState);
setStatus('Executing');
increment();
}
break;
case 'Executing':
if (!jobs.length) {
setStatus('Idle');
break;
}
if (currentIndex < jobs.length || result.result === 'Unknown')
break;
if (result.result === 'Failed') {
if (result.errors.length) {
if (onPipelineFailed)
onPipelineFailed(result.errors[0]);
if (events && events.onPipelineFailed)
events.onPipelineFailed(result.errors[0]);
}
}
else {
if (onPipelineSuccess)
onPipelineSuccess(result.values);
if (events && events.onPipelineSuccess)
events.onPipelineSuccess(result.values);
}
setStatus('Completed');
break;
case 'Completed':
{
const completedState = {
currentIndex: Math.min(currentIndex, jobs.length - 1),
jobs: jobs,
status,
name,
};
if (onPipelineFinished)
onPipelineFinished(result, completedState);
if (events && events.onPipelineFinished)
events.onPipelineFinished(result, completedState);
setStatus('Idle');
const resetStates = () => {
setEvents(null);
dispatch({
type: 'SET',
jobs: jobList
});
setResult(prev => ({
...prev,
errors: [],
result: 'Unknown',
values: null
}));
reset();
};
setTimeout(resetStates, 2000);
}
break;
default:
assertNever(status);
}
}, [status, result.result, currentIndex]);
useEffect(() => {
if (!isEqual(jobs, jobList))
dispatch({
type: 'SET',
jobs: jobList
});
}, [...(deps || [])]);
const operations = useMemo(() => {
const execute = (values, callbacks) => {
if (status === 'Idle') {
if (callbacks)
setEvents(callbacks);
dispatch({ type: 'SET_STATUS', status: 'Pending' });
setResult(prev => ({ ...prev, values }));
setStatus('Pending');
}
};
const executeAsync = (values) => (new Promise((resolve, reject) => execute(values, {
onPipelineSuccess: () => resolve(),
onPipelineFailed: reject
})));
return ({
run: execute,
runAsync: executeAsync,
start: () => execute(initialValues),
startAsync: () => executeAsync(initialValues)
});
}, [initialValues, status]);
return {
name,
jobs,
status,
currentIndex,
...operations
};
};
//# sourceMappingURL=usePipeline.js.map