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==", "integrity": "sha512-dsfE4BHJkLQW+reOS6b17xhZ/6FB1rB8eRRvO08nn5o+voxf3i74tuyFWNH6djdfgX7Sm5s6LD8t6mJug4dpDw==",
"dev": true "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": { "@types/pg": {
"version": "7.11.2", "version": "7.11.2",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-7.11.2.tgz", "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": { "object-visit": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz",
@ -5697,6 +5712,12 @@
"symbol-observable": "^1.2.0" "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": { "redux-saga": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.1.1.tgz", "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/http-errors": "^1.6.2",
"@types/luxon": "^1.15.2", "@types/luxon": "^1.15.2",
"@types/node": "^12.11.2", "@types/node": "^12.11.2",
"@types/object-hash": "^1.3.0",
"@types/pg": "^7.11.2", "@types/pg": "^7.11.2",
"@types/pino": "^5.8.12", "@types/pino": "^5.8.12",
"@types/react": "^16.9.9", "@types/react": "^16.9.9",
@ -57,6 +58,7 @@
"@types/webpack-dev-middleware": "^2.0.3", "@types/webpack-dev-middleware": "^2.0.3",
"@types/yargs": "^13.0.3", "@types/yargs": "^13.0.3",
"css-loader": "^3.2.0", "css-loader": "^3.2.0",
"object-hash": "^2.0.0",
"pg-monitor": "^1.3.1", "pg-monitor": "^1.3.1",
"pino-pretty": "^3.2.2", "pino-pretty": "^3.2.2",
"react": "^16.11.0", "react": "^16.11.0",
@ -65,6 +67,7 @@
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"redux": "^4.0.4", "redux": "^4.0.4",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.1.1", "redux-saga": "^1.1.1",
"ts-loader": "^6.2.0", "ts-loader": "^6.2.0",
"ts-node": "^8.4.1", "ts-node": "^8.4.1",

View File

@ -1,13 +1,16 @@
import { AppState } from "@kredens/frontend/store"; import { AppState } from "@kredens/frontend/store";
import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions"; import { deleteTask, scheduleTask } from "@kredens/frontend/store/tasks/actions";
import { Task, TaskScheduleType } from "@kredens/frontend/store/tasks/types"; 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"; import { useDispatch, useSelector } from "react-redux";
export default () => { export default () => {
const [taskName, setTaskName] = useState(""); const [taskName, setTaskName] = useState("");
const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items); const tasks = useSelector<AppState, { [key: string]: Task }>(state => state.tasks.items);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => {
dispatch({type: "FETCH_TASKS"});
}, [])
const onTaskAddClick = () => { const onTaskAddClick = () => {
dispatch(scheduleTask(Math.random().toString(36), { 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"; import { tasksReducer } from "./tasks/reducers";
const rootReducer = combineReducers({ const rootReducer = combineReducers({
@ -7,8 +10,14 @@ const rootReducer = combineReducers({
export type AppState = ReturnType<typeof rootReducer>; export type AppState = ReturnType<typeof rootReducer>;
const sagaMiddleware = createSagaMiddleware();
export default function configureStore() { export default function configureStore() {
const store = createStore(rootReducer); const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store; 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 { export function scheduleTask(id: string, task: Task): TasksActionType {
return { return {
@ -14,3 +15,35 @@ export function deleteTask(id: string): TasksActionType {
id 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"; import { TasksAction, TasksActionType, TasksState } from "./types";
const initialState: TasksState = { const initialState: TasksState = {
items: {} items: {},
queries: {}
}; };
export function tasksReducer( export function tasksReducer(
@ -24,6 +26,45 @@ export function tasksReducer(
.filter(([key]) => key !== action.id) .filter(([key]) => key !== action.id)
.reduce((res, [key, task]) => ({ ...res, [key]: task }), {}) .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: { default: {
return state; return state;
} }

View File

@ -19,26 +19,82 @@ export interface Task {
schedule: TaskSchedule; 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 { export interface TasksState {
items: { items: {
[key: string]: Task; [key: string]: Task;
}; };
queries: {
[key: string]: TaskQueryResult;
};
} }
export enum TasksAction { export enum TasksAction {
SCHEDULE_TASK = "SCHEDULE_TASK", 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 { interface ScheduleTaskAction {
type: typeof TasksAction.SCHEDULE_TASK; type: TasksAction.SCHEDULE_TASK;
id: string; id: string;
task: Task; task: Task;
} }
interface DeleteTaskAction { interface DeleteTaskAction {
type: typeof TasksAction.DELETE_TASK; type: TasksAction.DELETE_TASK;
id: string; 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", "extends": "../../tslint.json",
"rules": { "rules": {
"no-implicit-dependencies": [true, "dev", ["@kredens/frontend"]] "no-implicit-dependencies": [
} true,
"dev",
["@kredens/frontend"]
],
"no-submodule-imports": [
true,
"@kredens/frontend",
"redux-saga/effects"
]
}
} }

View File

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