Playing with sagas for a stuf implementation of tasks

This commit is contained in:
Gender Shrapnel 2019-10-24 04:12:49 +02:00
parent feda28ec47
commit 64b0cdc951
10 changed files with 245 additions and 31 deletions

21
package-lock.json generated
View File

@ -295,6 +295,15 @@
"integrity": "sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==",
"dev": true
},
"@types/object-hash": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-1.3.0.tgz",
"integrity": "sha512-il4NIe4jTx4lfhkKaksmmGHw5EsVkO8sHWkpJHM9m59r1dtsVadLSrJqdE8zU74NENDAsR3oLIOlooRAXlPLNA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/pg": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz",
@ -4652,6 +4661,12 @@
}
}
},
"object-hash": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.0.tgz",
"integrity": "sha512-I7zGBH0rDKwVGeGZpZoFaDhIwvJa3l1CZE+8VchylXbInNiCj7sxxea9P5dTM4ftKR5//nrqxrdeGSTWL2VpBA==",
"dev": true
},
"object-visit": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@ -5697,6 +5712,12 @@
"symbol-observable": "^1.2.0"
}
},
"redux-devtools-extension": {
"version": "2.13.8",
"resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz",
"integrity": "sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg==",
"dev": true
},
"redux-saga": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz",

View File

@ -47,6 +47,7 @@
"@types/http-errors": "^1.6.2",
"@types/luxon": "^1.15.2",
"@types/node": "^12.11.2",
"@types/object-hash": "^1.3.0",
"@types/pg": "^7.11.2",
"@types/pino": "^5.8.12",
"@types/react": "^16.9.9",
@ -57,6 +58,7 @@
"@types/webpack-dev-middleware": "^2.0.3",
"@types/yargs": "^13.0.3",
"css-loader": "^3.2.0",
"object-hash": "^2.0.0",
"pg-monitor": "^1.3.1",
"pino-pretty": "^3.2.2",
"react": "^16.11.0",
@ -65,6 +67,7 @@
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.1.1",
"ts-loader": "^6.2.0",
"ts-node": "^8.4.1",

View File

@ -1,13 +1,16 @@
import { AppState } from "@kredens/frontend/store";
import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions";
import { Task, TaskScheduleType } from "@kredens/frontend/store/tasks/types";
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
export default () => {
const [taskName, setTaskName] = useState("");
const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items);
const dispatch = useDispatch();
useEffect(() => {
dispatch({type: "FETCH_TASKS"});
}, [])
const onTaskAddClick = () => {
dispatch(scheduleTask(Math.random().toString(36), {

View File

@ -1,4 +1,7 @@
import { combineReducers, createStore } from "redux";
import { applyMiddleware, combineReducers, createStore } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import createSagaMiddleware from "redux-saga";
import rootSaga from "./sagas";
import { tasksReducer } from "./tasks/reducers";
const rootReducer = combineReducers({
@ -7,8 +10,14 @@ const rootReducer = combineReducers({
export type AppState = ReturnType<typeof rootReducer>;
const sagaMiddleware = createSagaMiddleware();
export default function configureStore() {
const store = createStore(rootReducer);
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}

View File

@ -0,0 +1,35 @@
import { all, call, put, takeEvery } from "redux-saga/effects";
import { getTasks, Task as APITask } from "../api/tasks";
import { taskFetchError, taskFetchOk, taskFetchStart } from "./tasks/actions";
import { Task, TaskQuery, TaskScheduleType } from "./tasks/types";
export function* fetchTasksSaga(query: TaskQuery = { limit: 10 }) {
yield put(taskFetchStart(query));
try {
const tasks: APITask[] = yield call(getTasks);
yield put(
taskFetchOk(
query,
tasks
.map<[string, Task]>(t => [
t.id.toString(),
{
name: t.name,
schedule: {
type: TaskScheduleType.Once,
due: t.due.toISO()
}
}
])
.reduce((res, [id, task]) => ({ ...res, [id]: task }), {})
)
);
} catch (error) {
yield put(taskFetchError(query, `${error}`));
}
}
export default function* rootSaga() {
yield all([yield takeEvery("FETCH_TASKS", fetchTasksSaga)]);
}

View File

@ -1,4 +1,5 @@
import { Task, TasksAction, TasksActionType } from "./types";
import { DateTime } from "luxon";
import { Task, TaskQuery, TasksAction, TasksActionType } from "./types";
export function scheduleTask(id: string, task: Task): TasksActionType {
return {
@ -14,3 +15,35 @@ export function deleteTask(id: string): TasksActionType {
id
};
}
export function taskFetchStart(query: TaskQuery): TasksActionType {
return {
type: TasksAction.TASKS_FETCH_START,
query,
started: DateTime.utc().toISO()
};
}
export function taskFetchOk(
query: TaskQuery,
results: { [key: string]: Task }
): TasksActionType {
return {
type: TasksAction.TASKS_FETCH_OK,
query,
results,
fetched: DateTime.utc().toISO()
};
}
export function taskFetchError(
query: TaskQuery,
error: string
): TasksActionType {
return {
type: TasksAction.TASKS_FETCH_ERROR,
query,
error,
fetched: DateTime.utc().toISO()
};
}

View File

@ -1,7 +1,9 @@
import objectHash from "object-hash";
import { TasksAction, TasksActionType, TasksState } from "./types";
const initialState: TasksState = {
items: {}
items: {},
queries: {}
};
export function tasksReducer(
@ -24,6 +26,45 @@ export function tasksReducer(
.filter(([key]) => key !== action.id)
.reduce((res, [key, task]) => ({ ...res, [key]: task }), {})
};
case TasksAction.TASKS_FETCH_START:
return {
...state,
queries: {
...state.queries,
[objectHash(action.query)]: {
result: "fetching",
started: action.started
}
}
};
case TasksAction.TASKS_FETCH_OK:
return {
...state,
items: {
...state.items,
...action.results
},
queries: {
...state.queries,
[objectHash(action.query)]: {
result: "ok",
items: Object.keys(action.results),
fetched: action.fetched
}
}
};
case TasksAction.TASKS_FETCH_ERROR:
return {
...state,
queries: {
...state.queries,
[objectHash(action.query)]: {
result: "error",
error: action.error,
fetched: action.fetched
}
}
};
default: {
return state;
}

View File

@ -19,26 +19,82 @@ export interface Task {
schedule: TaskSchedule;
}
export interface TaskQuery {
query?: string;
after?: string;
limit: number;
}
interface TaskQueryOk {
result: "ok";
items: string[];
fetched: string;
}
interface TaskQueryFetching {
result: "fetching";
started: string;
}
interface TaskQueryError {
result: "error";
error: string;
fetched: string;
}
export type TaskQueryResult = TaskQueryOk | TaskQueryFetching | TaskQueryError;
export interface TasksState {
items: {
[key: string]: Task;
};
queries: {
[key: string]: TaskQueryResult;
};
}
export enum TasksAction {
SCHEDULE_TASK = "SCHEDULE_TASK",
DELETE_TASK = "DELETE_TASK"
DELETE_TASK = "DELETE_TASK",
TASKS_FETCH_START = "TASKS_FETCH_START",
TASKS_FETCH_OK = "TASKS_FETCH_OK",
TASKS_FETCH_ERROR = "TASKS_FETCH_ERROR"
}
interface ScheduleTaskAction {
type: typeof TasksAction.SCHEDULE_TASK;
type: TasksAction.SCHEDULE_TASK;
id: string;
task: Task;
}
interface DeleteTaskAction {
type: typeof TasksAction.DELETE_TASK;
type: TasksAction.DELETE_TASK;
id: string;
}
export type TasksActionType = ScheduleTaskAction | DeleteTaskAction;
interface TaskFetchStartAction {
type: TasksAction.TASKS_FETCH_START;
query: TaskQuery;
started: string;
}
interface TaskFetchOkAction {
type: TasksAction.TASKS_FETCH_OK;
query: TaskQuery;
results: { [key: string]: Task };
fetched: string;
}
interface TaskFetchErrorAction {
type: TasksAction.TASKS_FETCH_ERROR;
query: TaskQuery;
error: string;
fetched: string;
}
export type TasksActionType =
| ScheduleTaskAction
| DeleteTaskAction
| TaskFetchOkAction
| TaskFetchStartAction
| TaskFetchErrorAction;

View File

@ -1,6 +1,15 @@
{
"extends": "../../tslint.json",
"rules": {
"no-implicit-dependencies": [true, "dev", ["@kredens/frontend"]]
}
}
"extends": "../../tslint.json",
"rules": {
"no-implicit-dependencies": [
true,
"dev",
["@kredens/frontend"]
],
"no-submodule-imports": [
true,
"@kredens/frontend",
"redux-saga/effects"
]
}
}

View File

@ -1,19 +1,23 @@
{
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-config-prettier"],
"jsRules": {},
"rules": {
"interface-name": [true, "never-prefix"],
"no-submodule-imports": [
true,
"graphql/language",
"graphql/type",
"vue-loader/lib/plugin",
"@kredens"
],
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
"object-literal-sort-keys": [true, "match-declaration-order-only", "shorthand-first"],
"max-classes-per-file": false
},
"rulesDirectory": []
"defaultSeverity": "error",
"extends": ["tslint:latest", "tslint-config-prettier"],
"jsRules": {},
"rules": {
"interface-name": [true, "never-prefix"],
"no-submodule-imports": [
true,
"graphql/language",
"graphql/type",
"vue-loader/lib/plugin",
"@kredens"
],
"no-implicit-dependencies": [true, ["@kredens", "@kredens/frontend"]],
"object-literal-sort-keys": [
true,
"match-declaration-order-only",
"shorthand-first"
],
"max-classes-per-file": false
},
"rulesDirectory": []
}