@zeniai/client-epic-state 5.0.3-beta0ND → 5.0.3-betaND1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/coreEpics.js +1 -2
- package/lib/entity/tenant/tenantReducer.d.ts +1 -5
- package/lib/entity/tenant/tenantReducer.js +2 -23
- package/lib/entity/tenant/tenantState.d.ts +0 -1
- package/lib/esm/coreEpics.js +1 -2
- package/lib/esm/entity/tenant/tenantReducer.js +1 -21
- package/lib/esm/index.js +2 -5
- package/lib/esm/view/taskManager/taskListView/epics/fetchTaskListEpic.js +6 -3
- package/lib/esm/view/taskManager/taskListView/taskListPayload.js +5 -1
- package/lib/esm/view/taskManager/taskListView/taskListReducer.js +11 -0
- package/lib/esm/view/taskManager/taskListView/taskListSelector.js +27 -4
- package/lib/index.d.ts +2 -5
- package/lib/index.js +30 -36
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -1
- package/lib/view/taskManager/taskListView/epics/fetchTaskListEpic.js +6 -3
- package/lib/view/taskManager/taskListView/taskList.d.ts +2 -0
- package/lib/view/taskManager/taskListView/taskListPayload.d.ts +10 -1
- package/lib/view/taskManager/taskListView/taskListPayload.js +7 -0
- package/lib/view/taskManager/taskListView/taskListReducer.d.ts +2 -1
- package/lib/view/taskManager/taskListView/taskListReducer.js +11 -0
- package/lib/view/taskManager/taskListView/taskListSelector.d.ts +4 -1
- package/lib/view/taskManager/taskListView/taskListSelector.js +27 -4
- package/package.json +1 -1
- package/lib/entity/tenant/SessionManager.d.ts +0 -38
- package/lib/entity/tenant/SessionManager.js +0 -171
- package/lib/entity/tenant/epic/sessionHeartbeatEpic.d.ts +0 -16
- package/lib/entity/tenant/epic/sessionHeartbeatEpic.js +0 -16
- package/lib/entity/tenant/sessionTypes.d.ts +0 -26
- package/lib/entity/tenant/sessionTypes.js +0 -12
- package/lib/esm/entity/tenant/SessionManager.js +0 -167
- package/lib/esm/entity/tenant/epic/sessionHeartbeatEpic.js +0 -12
- package/lib/esm/entity/tenant/sessionTypes.js +0 -9
|
@@ -6,16 +6,19 @@ const operators_1 = require("rxjs/operators");
|
|
|
6
6
|
const tagReducer_1 = require("../../../../entity/tag/tagReducer");
|
|
7
7
|
const taskReducer_1 = require("../../../../entity/task/taskReducer");
|
|
8
8
|
const responsePayload_1 = require("../../../../responsePayload");
|
|
9
|
+
const taskListPayload_1 = require("../taskListPayload");
|
|
9
10
|
const taskListReducer_1 = require("../taskListReducer");
|
|
10
11
|
const fetchTaskListEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(taskListReducer_1.fetchTaskList.match), (0, operators_1.switchMap)(() => {
|
|
11
12
|
return zeniAPI
|
|
12
13
|
.getJSON(`${zeniAPI.apiEndPoints.taskMicroServiceBaseUrl}/1.0/task-manager/tasks?query=${encodeURIComponent(`{"task_type": "all"}`)}`)
|
|
13
14
|
.pipe((0, operators_1.mergeMap)((response) => {
|
|
14
15
|
if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const { tasks } = response.data;
|
|
17
|
+
const tasksForEntity = (0, taskListPayload_1.flattenTasksAndSubtasksForEntity)(tasks);
|
|
18
|
+
return (0, rxjs_1.of)((0, taskReducer_1.updateTasks)(tasksForEntity), (0, taskListReducer_1.updateTaskList)({
|
|
19
|
+
data: tasks,
|
|
17
20
|
updateType: 'replace',
|
|
18
|
-
}), (0, tagReducer_1.updateTags)(
|
|
21
|
+
}), (0, tagReducer_1.updateTags)(tasksForEntity.map((task) => task.tags).flat()));
|
|
19
22
|
}
|
|
20
23
|
else {
|
|
21
24
|
return (0, rxjs_1.of)((0, taskListReducer_1.updateTaskListFetchStatus)({
|
|
@@ -15,6 +15,8 @@ export declare type DueDateGroupKey = ReturnType<typeof toDueDateGroupKeyType>;
|
|
|
15
15
|
export interface TaskListState extends FetchedState {
|
|
16
16
|
filters: TaskListFilters;
|
|
17
17
|
localData: TaskListLocalData;
|
|
18
|
+
/** Maps each list (parent) task id to ordered subtask ids from the last list fetch. */
|
|
19
|
+
subtaskIdsByParentTaskId: Record<ID, ID[]>;
|
|
18
20
|
taskIds: ID[];
|
|
19
21
|
taskIdsByAssignees: Record<ID, ID[]>;
|
|
20
22
|
taskIdsByDueDate: Record<DueDateGroupKey, ID[]>;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { TaskPayload } from '../../../entity/task/taskPayload';
|
|
2
2
|
import { ZeniAPIResponse } from '../../../responsePayload';
|
|
3
|
+
/**
|
|
4
|
+
* Task row returned by the task list API. Subtasks share the same wire shape
|
|
5
|
+
* as tasks but are nested under their parent and indexed separately in list state.
|
|
6
|
+
*/
|
|
7
|
+
export interface TaskListTaskPayload extends TaskPayload {
|
|
8
|
+
subtasks?: TaskPayload[];
|
|
9
|
+
}
|
|
3
10
|
interface FetchTaskListPayload {
|
|
4
|
-
tasks:
|
|
11
|
+
tasks: TaskListTaskPayload[];
|
|
5
12
|
}
|
|
6
13
|
export type FetchTaskListResponse = ZeniAPIResponse<FetchTaskListPayload>;
|
|
14
|
+
/** Parent tasks and nested subtasks as flat `TaskPayload`s for `updateTasks`. */
|
|
15
|
+
export declare const flattenTasksAndSubtasksForEntity: (tasks: TaskListTaskPayload[]) => TaskPayload[];
|
|
7
16
|
export {};
|
|
@@ -1,2 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.flattenTasksAndSubtasksForEntity = void 0;
|
|
4
|
+
/** Parent tasks and nested subtasks as flat `TaskPayload`s for `updateTasks`. */
|
|
5
|
+
const flattenTasksAndSubtasksForEntity = (tasks) => tasks.flatMap((task) => {
|
|
6
|
+
const { subtasks, ...parent } = task;
|
|
7
|
+
return [parent, ...(subtasks ?? [])];
|
|
8
|
+
});
|
|
9
|
+
exports.flattenTasksAndSubtasksForEntity = flattenTasksAndSubtasksForEntity;
|
|
@@ -5,12 +5,13 @@ import { TaskGroupPayload } from '../../../entity/taskGroup/taskGroupPayload';
|
|
|
5
5
|
import { TaskGroup } from '../../../entity/taskGroup/taskGroupState';
|
|
6
6
|
import { ZeniAPIStatus } from '../../../responsePayload';
|
|
7
7
|
import { TaskListFilters, TaskListLocalData, TaskListState, TaskListUIState } from './taskList';
|
|
8
|
+
import { TaskListTaskPayload } from './taskListPayload';
|
|
8
9
|
export declare const initialState: TaskListState;
|
|
9
10
|
export declare const fetchTaskListPage: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[cacheOverride: boolean, isTaskTemplatesEnabled: boolean], {
|
|
10
11
|
cacheOverride: boolean;
|
|
11
12
|
isTaskTemplatesEnabled: boolean;
|
|
12
13
|
}, "taskList/fetchTaskListPage", never, never>, fetchTaskList: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"taskList/fetchTaskList">, updateTaskList: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
13
|
-
data:
|
|
14
|
+
data: TaskListTaskPayload[];
|
|
14
15
|
updateType: UpdateType;
|
|
15
16
|
}, "taskList/updateTaskList">, updateTaskListFetchStatus: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
|
|
16
17
|
fetchState: FetchState;
|
|
@@ -34,6 +34,7 @@ const initialTaskIdsByDueDate = {
|
|
|
34
34
|
upcoming: [],
|
|
35
35
|
};
|
|
36
36
|
exports.initialState = {
|
|
37
|
+
subtaskIdsByParentTaskId: {},
|
|
37
38
|
taskIds: [],
|
|
38
39
|
taskIdsByGroupsIds: {},
|
|
39
40
|
taskIdsByPriority: initialTaskIdsByPriority,
|
|
@@ -93,6 +94,7 @@ const taskList = (0, toolkit_1.createSlice)({
|
|
|
93
94
|
const { data } = action.payload;
|
|
94
95
|
draft.fetchState = 'Completed';
|
|
95
96
|
draft.error = undefined;
|
|
97
|
+
draft.subtaskIdsByParentTaskId = buildSubtaskIdsByParentTaskId(data);
|
|
96
98
|
const { taskIds, taskIdsGroupedByPriority, taskIdsByGroupIds, taskIdsGroupedByStatus, taskIdsByAssignees, taskIdsByDueDate, taskIdsByTags, } = getGroupedTaskIds(data);
|
|
97
99
|
draft.taskIds = taskIds;
|
|
98
100
|
draft.taskIdsByGroupsIds = taskIdsByGroupIds;
|
|
@@ -492,6 +494,15 @@ const getTagsGroupKey = (tags) => {
|
|
|
492
494
|
tagIds.sort();
|
|
493
495
|
return tagIds.join(',');
|
|
494
496
|
};
|
|
497
|
+
const buildSubtaskIdsByParentTaskId = (tasks) => {
|
|
498
|
+
const subtaskIdsByParentTaskId = {};
|
|
499
|
+
tasks.forEach((task) => {
|
|
500
|
+
if (task.subtasks != null && task.subtasks.length > 0) {
|
|
501
|
+
subtaskIdsByParentTaskId[task.task_id] = task.subtasks.map((s) => s.task_id);
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
return subtaskIdsByParentTaskId;
|
|
505
|
+
};
|
|
495
506
|
const getGroupedTaskIds = (tasks) => {
|
|
496
507
|
const taskIds = [];
|
|
497
508
|
const taskIdsByGroupIds = {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FetchStateAndError } from '../../../commonStateTypes/common';
|
|
1
|
+
import { FetchStateAndError, ID } from '../../../commonStateTypes/common';
|
|
2
2
|
import { SelectorView } from '../../../commonStateTypes/viewAndReport/viewAndReport';
|
|
3
3
|
import { AddressState } from '../../../entity/address/addressState';
|
|
4
4
|
import { TagState } from '../../../entity/tag/tagState';
|
|
@@ -13,6 +13,7 @@ import { UserListSelectorView } from '../../userListView/userListViewSelector';
|
|
|
13
13
|
import { TaskListFilters, TaskListLocalData, TaskListState, TaskListUIState } from './taskList';
|
|
14
14
|
export interface TaskWithUserDetails extends Task {
|
|
15
15
|
assigneeUsers: UserAndRole[];
|
|
16
|
+
subtasks: TaskWithUserDetails[];
|
|
16
17
|
}
|
|
17
18
|
export interface TaskGroupWithTasksList {
|
|
18
19
|
groupName: string;
|
|
@@ -23,6 +24,8 @@ export interface TaskGroupWithTasksList {
|
|
|
23
24
|
export interface TaskListSelectorView extends SelectorView {
|
|
24
25
|
filters: TaskListFilters;
|
|
25
26
|
localData: TaskListLocalData;
|
|
27
|
+
/** Subtask ids grouped by parent list task id (from the task list API). */
|
|
28
|
+
subtaskIdsByParentTaskId: Record<ID, ID[]>;
|
|
26
29
|
taskCreationFromTemplateStatus: FetchStateAndError;
|
|
27
30
|
taskGroupCreationStatus: FetchStateAndError;
|
|
28
31
|
taskGroupDeleteStatus: FetchStateAndError;
|
|
@@ -49,6 +49,30 @@ const sortTasksList = (tagState, tasksList, sortKey, sortOrder) => {
|
|
|
49
49
|
}, sortOrder);
|
|
50
50
|
return tasksInOrder;
|
|
51
51
|
};
|
|
52
|
+
const taskToTaskWithUserDetails = (task, userState, userRoleState, addressState) => ({
|
|
53
|
+
...task,
|
|
54
|
+
assigneeUsers: task.assignees
|
|
55
|
+
.map((userId) => (0, userAndRole_1.getUserAndUserRole)(userState, userRoleState, addressState, userId))
|
|
56
|
+
.filter((value) => value != null),
|
|
57
|
+
subtasks: [],
|
|
58
|
+
});
|
|
59
|
+
const attachSubtasksForTask = (taskWithAssignees, taskListState, taskState, userState, userRoleState, addressState) => {
|
|
60
|
+
const subtaskIds = taskListState.subtaskIdsByParentTaskId[taskWithAssignees.id] ?? [];
|
|
61
|
+
if (subtaskIds.length === 0) {
|
|
62
|
+
return { ...taskWithAssignees, subtasks: [] };
|
|
63
|
+
}
|
|
64
|
+
const subtasks = subtaskIds
|
|
65
|
+
.map((subtaskId) => {
|
|
66
|
+
const subtask = (0, taskSelector_1.getTaskById)(taskState, subtaskId);
|
|
67
|
+
if (subtask == null) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const withAssignees = taskToTaskWithUserDetails(subtask, userState, userRoleState, addressState);
|
|
71
|
+
return attachSubtasksForTask(withAssignees, taskListState, taskState, userState, userRoleState, addressState);
|
|
72
|
+
})
|
|
73
|
+
.filter((value) => value != null);
|
|
74
|
+
return { ...taskWithAssignees, subtasks };
|
|
75
|
+
};
|
|
52
76
|
const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRoleState, addressState, taskIds, tagState, }) => {
|
|
53
77
|
const { sortKey, searchText } = taskListState.uiState;
|
|
54
78
|
const sortOrder = (0, sortOrderPayload_1.getSortOrder)(taskListState.uiState.sortOrder);
|
|
@@ -56,10 +80,8 @@ const getTasksWithUserDetails = ({ taskListState, taskState, userState, userRole
|
|
|
56
80
|
.map((taskId) => {
|
|
57
81
|
const task = (0, taskSelector_1.getTaskById)(taskState, taskId);
|
|
58
82
|
if (task != null) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
.filter((value) => value != null);
|
|
62
|
-
return { ...task, assigneeUsers };
|
|
83
|
+
const taskWithAssignees = taskToTaskWithUserDetails(task, userState, userRoleState, addressState);
|
|
84
|
+
return attachSubtasksForTask(taskWithAssignees, taskListState, taskState, userState, userRoleState, addressState);
|
|
63
85
|
}
|
|
64
86
|
return undefined;
|
|
65
87
|
})
|
|
@@ -342,6 +364,7 @@ exports.getAllTasks = (0, toolkit_1.createSelector)((state) => state.taskListSta
|
|
|
342
364
|
return {
|
|
343
365
|
uiState: taskListState.uiState,
|
|
344
366
|
filters: taskListState.filters,
|
|
367
|
+
subtaskIdsByParentTaskId: taskListState.subtaskIdsByParentTaskId,
|
|
345
368
|
taskGroupTemplates,
|
|
346
369
|
fetchState: fetchState,
|
|
347
370
|
userList: (0, userListViewSelector_1.getUserList)(userState, userRoleState, userListViewState, 'taskManagerCandidate'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.0.3-
|
|
3
|
+
"version": "5.0.3-betaND1",
|
|
4
4
|
"description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "lib/esm/index.js",
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionManager — Central session lifecycle manager.
|
|
3
|
-
*
|
|
4
|
-
* Responsibilities:
|
|
5
|
-
* 1. Track user activity (mousemove, keydown, click, scroll, touch)
|
|
6
|
-
* 2. Send heartbeat at configurable intervals while user is active
|
|
7
|
-
* 3. Show warning popup after idle timeout
|
|
8
|
-
* 4. Auto-logout after warning countdown expires
|
|
9
|
-
*/
|
|
10
|
-
import { SessionCallbacks, SessionConfig } from './sessionTypes';
|
|
11
|
-
export declare class SessionManager {
|
|
12
|
-
private config;
|
|
13
|
-
private callbacks;
|
|
14
|
-
/** Timestamps and timers */
|
|
15
|
-
private lastActivityTime;
|
|
16
|
-
private idleCheckInterval;
|
|
17
|
-
private heartbeatInterval;
|
|
18
|
-
private countdownInterval;
|
|
19
|
-
private activityDebounceTimer;
|
|
20
|
-
/** State */
|
|
21
|
-
private running;
|
|
22
|
-
private warningActive;
|
|
23
|
-
private secondsRemaining;
|
|
24
|
-
/** Bound handler for event listener cleanup. */
|
|
25
|
-
private boundOnActivity;
|
|
26
|
-
private boundOnVisibilityChange;
|
|
27
|
-
start(config: Partial<SessionConfig> | undefined, callbacks: SessionCallbacks): void;
|
|
28
|
-
stop(): void;
|
|
29
|
-
continueSession(): void;
|
|
30
|
-
isWarningActive(): boolean;
|
|
31
|
-
getSecondsRemaining(): number;
|
|
32
|
-
private onActivity;
|
|
33
|
-
private onVisibilityChange;
|
|
34
|
-
private checkIdle;
|
|
35
|
-
private startWarning;
|
|
36
|
-
private startHeartbeat;
|
|
37
|
-
private clearTimer;
|
|
38
|
-
}
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* SessionManager — Central session lifecycle manager.
|
|
4
|
-
*
|
|
5
|
-
* Responsibilities:
|
|
6
|
-
* 1. Track user activity (mousemove, keydown, click, scroll, touch)
|
|
7
|
-
* 2. Send heartbeat at configurable intervals while user is active
|
|
8
|
-
* 3. Show warning popup after idle timeout
|
|
9
|
-
* 4. Auto-logout after warning countdown expires
|
|
10
|
-
*/
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.SessionManager = void 0;
|
|
13
|
-
const sessionTypes_1 = require("./sessionTypes");
|
|
14
|
-
/** Events that indicate user activity. */
|
|
15
|
-
const ACTIVITY_EVENTS = [
|
|
16
|
-
'mousemove',
|
|
17
|
-
'mousedown',
|
|
18
|
-
'keydown',
|
|
19
|
-
'scroll',
|
|
20
|
-
'touchstart',
|
|
21
|
-
'click',
|
|
22
|
-
];
|
|
23
|
-
/** Debounce interval for activity events (ms). */
|
|
24
|
-
const ACTIVITY_DEBOUNCE_MS = 1000;
|
|
25
|
-
class SessionManager {
|
|
26
|
-
constructor() {
|
|
27
|
-
this.config = sessionTypes_1.DEFAULT_SESSION_CONFIG;
|
|
28
|
-
this.callbacks = null;
|
|
29
|
-
/** Timestamps and timers */
|
|
30
|
-
this.lastActivityTime = 0;
|
|
31
|
-
this.idleCheckInterval = null;
|
|
32
|
-
this.heartbeatInterval = null;
|
|
33
|
-
this.countdownInterval = null;
|
|
34
|
-
this.activityDebounceTimer = null;
|
|
35
|
-
/** State */
|
|
36
|
-
this.running = false;
|
|
37
|
-
this.warningActive = false;
|
|
38
|
-
this.secondsRemaining = 0;
|
|
39
|
-
/** Bound handler for event listener cleanup. */
|
|
40
|
-
this.boundOnActivity = this.onActivity.bind(this);
|
|
41
|
-
this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);
|
|
42
|
-
}
|
|
43
|
-
// ─── Public API ────────────────────────────────────────────
|
|
44
|
-
start(config = {}, callbacks) {
|
|
45
|
-
if (this.running) {
|
|
46
|
-
this.stop();
|
|
47
|
-
}
|
|
48
|
-
this.config = { ...sessionTypes_1.DEFAULT_SESSION_CONFIG, ...config };
|
|
49
|
-
this.callbacks = callbacks;
|
|
50
|
-
this.running = true;
|
|
51
|
-
this.warningActive = false;
|
|
52
|
-
this.lastActivityTime = Date.now();
|
|
53
|
-
if (!this.config.isAutoLogoutEnabled) {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
ACTIVITY_EVENTS.forEach((event) => {
|
|
57
|
-
document.addEventListener(event, this.boundOnActivity, {
|
|
58
|
-
capture: true,
|
|
59
|
-
passive: true,
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
document.addEventListener('visibilitychange', this.boundOnVisibilityChange);
|
|
63
|
-
this.startHeartbeat();
|
|
64
|
-
this.callbacks?.onHeartbeat();
|
|
65
|
-
this.idleCheckInterval = setInterval(() => this.checkIdle(), 1000);
|
|
66
|
-
}
|
|
67
|
-
stop() {
|
|
68
|
-
this.running = false;
|
|
69
|
-
this.warningActive = false;
|
|
70
|
-
ACTIVITY_EVENTS.forEach((event) => {
|
|
71
|
-
document.removeEventListener(event, this.boundOnActivity, {
|
|
72
|
-
capture: true,
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
document.removeEventListener('visibilitychange', this.boundOnVisibilityChange);
|
|
76
|
-
this.clearTimer('idleCheckInterval');
|
|
77
|
-
this.clearTimer('heartbeatInterval');
|
|
78
|
-
this.clearTimer('countdownInterval');
|
|
79
|
-
this.clearTimer('activityDebounceTimer');
|
|
80
|
-
}
|
|
81
|
-
continueSession() {
|
|
82
|
-
if (!this.running) {
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
this.warningActive = false;
|
|
86
|
-
this.secondsRemaining = 0;
|
|
87
|
-
this.lastActivityTime = Date.now();
|
|
88
|
-
this.clearTimer('countdownInterval');
|
|
89
|
-
this.callbacks?.onHeartbeat();
|
|
90
|
-
this.callbacks?.onSessionExtended();
|
|
91
|
-
this.startHeartbeat();
|
|
92
|
-
}
|
|
93
|
-
isWarningActive() {
|
|
94
|
-
return this.warningActive;
|
|
95
|
-
}
|
|
96
|
-
getSecondsRemaining() {
|
|
97
|
-
return this.secondsRemaining;
|
|
98
|
-
}
|
|
99
|
-
// ─── Private ───────────────────────────────────────────────
|
|
100
|
-
onActivity() {
|
|
101
|
-
if (!this.running || this.warningActive) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
if (this.activityDebounceTimer != null) {
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
this.lastActivityTime = Date.now();
|
|
108
|
-
this.activityDebounceTimer = setTimeout(() => {
|
|
109
|
-
this.activityDebounceTimer = null;
|
|
110
|
-
}, ACTIVITY_DEBOUNCE_MS);
|
|
111
|
-
}
|
|
112
|
-
onVisibilityChange() {
|
|
113
|
-
if (!this.running) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
if (document.visibilityState === 'visible') {
|
|
117
|
-
this.lastActivityTime = Date.now();
|
|
118
|
-
this.checkIdle();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
checkIdle() {
|
|
122
|
-
if (!this.running || this.warningActive) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const idleMs = Date.now() - this.lastActivityTime;
|
|
126
|
-
const idleTimeoutMs = this.config.idleTimeoutMinutes * 60 * 1000;
|
|
127
|
-
if (idleMs >= idleTimeoutMs) {
|
|
128
|
-
this.startWarning();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
startWarning() {
|
|
132
|
-
this.warningActive = true;
|
|
133
|
-
this.secondsRemaining = this.config.warningDurationSeconds;
|
|
134
|
-
this.clearTimer('heartbeatInterval');
|
|
135
|
-
this.callbacks?.onWarningStart(this.secondsRemaining);
|
|
136
|
-
this.countdownInterval = setInterval(() => {
|
|
137
|
-
this.secondsRemaining -= 1;
|
|
138
|
-
if (this.secondsRemaining <= 0) {
|
|
139
|
-
this.clearTimer('countdownInterval');
|
|
140
|
-
this.warningActive = false;
|
|
141
|
-
this.callbacks?.onAutoLogout();
|
|
142
|
-
}
|
|
143
|
-
else {
|
|
144
|
-
this.callbacks?.onWarningTick(this.secondsRemaining);
|
|
145
|
-
}
|
|
146
|
-
}, 1000);
|
|
147
|
-
}
|
|
148
|
-
startHeartbeat() {
|
|
149
|
-
this.clearTimer('heartbeatInterval');
|
|
150
|
-
const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
|
|
151
|
-
this.heartbeatInterval = setInterval(() => {
|
|
152
|
-
if (!this.running || this.warningActive) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
this.callbacks?.onHeartbeat();
|
|
156
|
-
}, intervalMs);
|
|
157
|
-
}
|
|
158
|
-
clearTimer(name) {
|
|
159
|
-
const timer = this[name];
|
|
160
|
-
if (timer != null) {
|
|
161
|
-
if (name === 'activityDebounceTimer') {
|
|
162
|
-
clearTimeout(timer);
|
|
163
|
-
}
|
|
164
|
-
else {
|
|
165
|
-
clearInterval(timer);
|
|
166
|
-
}
|
|
167
|
-
this[name] = null;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
exports.SessionManager = SessionManager;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ActionsObservable, StateObservable } from 'redux-observable';
|
|
2
|
-
import { RootState } from '../../../reducer';
|
|
3
|
-
import { ZeniAPI } from '../../../zeniAPI';
|
|
4
|
-
import { sendSessionHeartbeat, sessionHeartbeatFailure, sessionHeartbeatSuccess } from '../tenantReducer';
|
|
5
|
-
export type ActionType = ReturnType<typeof sendSessionHeartbeat> | ReturnType<typeof sessionHeartbeatSuccess> | ReturnType<typeof sessionHeartbeatFailure>;
|
|
6
|
-
export declare const sessionHeartbeatEpic: (actions$: ActionsObservable<ActionType>, _: StateObservable<RootState>, zeniAPI: ZeniAPI) => import("rxjs").Observable<{
|
|
7
|
-
payload: {
|
|
8
|
-
expiresAt: string;
|
|
9
|
-
};
|
|
10
|
-
type: "tenant/sessionHeartbeatSuccess";
|
|
11
|
-
} | {
|
|
12
|
-
payload: {
|
|
13
|
-
error: import("../../../responsePayload").ZeniAPIStatus<Record<string, unknown>>;
|
|
14
|
-
};
|
|
15
|
-
type: "tenant/sessionHeartbeatFailure";
|
|
16
|
-
}>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.sessionHeartbeatEpic = void 0;
|
|
4
|
-
const rxjs_1 = require("rxjs");
|
|
5
|
-
const operators_1 = require("rxjs/operators");
|
|
6
|
-
const responsePayload_1 = require("../../../responsePayload");
|
|
7
|
-
const tenantReducer_1 = require("../tenantReducer");
|
|
8
|
-
const sessionHeartbeatEpic = (actions$, _, zeniAPI) => actions$.pipe((0, operators_1.filter)(tenantReducer_1.sendSessionHeartbeat.match), (0, operators_1.switchMap)(() => zeniAPI
|
|
9
|
-
.postAndGetJSON(`${zeniAPI.apiEndPoints.authMicroServiceBaseUrl}/1.0/sessions/heartbeat`)
|
|
10
|
-
.pipe((0, operators_1.mergeMap)((response) => {
|
|
11
|
-
if ((0, responsePayload_1.isSuccessResponse)(response)) {
|
|
12
|
-
return (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatSuccess)(response.data?.expiry ?? ''));
|
|
13
|
-
}
|
|
14
|
-
return (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatFailure)(response.status));
|
|
15
|
-
}), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, tenantReducer_1.sessionHeartbeatFailure)((0, responsePayload_1.createZeniAPIStatus)('Heartbeat failed', 'Session heartbeat API call errored: ' + JSON.stringify(error))))))));
|
|
16
|
-
exports.sessionHeartbeatEpic = sessionHeartbeatEpic;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session management types for idle timeout and heartbeat.
|
|
3
|
-
*/
|
|
4
|
-
export interface SessionConfig {
|
|
5
|
-
/** Minutes between heartbeat API calls while user is active. Default: 10 */
|
|
6
|
-
heartbeatIntervalMinutes: number;
|
|
7
|
-
/** Minutes of inactivity before the warning popup appears. Default: 30 */
|
|
8
|
-
idleTimeoutMinutes: number;
|
|
9
|
-
/** Whether auto-logout is enabled. Default: true */
|
|
10
|
-
isAutoLogoutEnabled: boolean;
|
|
11
|
-
/** Seconds to count down in the warning popup before auto-logout. Default: 60 */
|
|
12
|
-
warningDurationSeconds: number;
|
|
13
|
-
}
|
|
14
|
-
export declare const DEFAULT_SESSION_CONFIG: SessionConfig;
|
|
15
|
-
export interface SessionCallbacks {
|
|
16
|
-
/** Called when the countdown reaches zero — app should sign out. */
|
|
17
|
-
onAutoLogout: () => void;
|
|
18
|
-
/** Called to send a heartbeat API request. */
|
|
19
|
-
onHeartbeat: () => void;
|
|
20
|
-
/** Called when the session is extended (heartbeat success or "Continue Session"). */
|
|
21
|
-
onSessionExtended: () => void;
|
|
22
|
-
/** Called when the warning countdown starts. */
|
|
23
|
-
onWarningStart: (secondsRemaining: number) => void;
|
|
24
|
-
/** Called every second during the countdown. */
|
|
25
|
-
onWarningTick: (secondsRemaining: number) => void;
|
|
26
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Session management types for idle timeout and heartbeat.
|
|
4
|
-
*/
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEFAULT_SESSION_CONFIG = void 0;
|
|
7
|
-
exports.DEFAULT_SESSION_CONFIG = {
|
|
8
|
-
heartbeatIntervalMinutes: 10,
|
|
9
|
-
idleTimeoutMinutes: 30,
|
|
10
|
-
isAutoLogoutEnabled: true,
|
|
11
|
-
warningDurationSeconds: 60,
|
|
12
|
-
};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SessionManager — Central session lifecycle manager.
|
|
3
|
-
*
|
|
4
|
-
* Responsibilities:
|
|
5
|
-
* 1. Track user activity (mousemove, keydown, click, scroll, touch)
|
|
6
|
-
* 2. Send heartbeat at configurable intervals while user is active
|
|
7
|
-
* 3. Show warning popup after idle timeout
|
|
8
|
-
* 4. Auto-logout after warning countdown expires
|
|
9
|
-
*/
|
|
10
|
-
import { DEFAULT_SESSION_CONFIG, } from './sessionTypes';
|
|
11
|
-
/** Events that indicate user activity. */
|
|
12
|
-
const ACTIVITY_EVENTS = [
|
|
13
|
-
'mousemove',
|
|
14
|
-
'mousedown',
|
|
15
|
-
'keydown',
|
|
16
|
-
'scroll',
|
|
17
|
-
'touchstart',
|
|
18
|
-
'click',
|
|
19
|
-
];
|
|
20
|
-
/** Debounce interval for activity events (ms). */
|
|
21
|
-
const ACTIVITY_DEBOUNCE_MS = 1000;
|
|
22
|
-
export class SessionManager {
|
|
23
|
-
constructor() {
|
|
24
|
-
this.config = DEFAULT_SESSION_CONFIG;
|
|
25
|
-
this.callbacks = null;
|
|
26
|
-
/** Timestamps and timers */
|
|
27
|
-
this.lastActivityTime = 0;
|
|
28
|
-
this.idleCheckInterval = null;
|
|
29
|
-
this.heartbeatInterval = null;
|
|
30
|
-
this.countdownInterval = null;
|
|
31
|
-
this.activityDebounceTimer = null;
|
|
32
|
-
/** State */
|
|
33
|
-
this.running = false;
|
|
34
|
-
this.warningActive = false;
|
|
35
|
-
this.secondsRemaining = 0;
|
|
36
|
-
/** Bound handler for event listener cleanup. */
|
|
37
|
-
this.boundOnActivity = this.onActivity.bind(this);
|
|
38
|
-
this.boundOnVisibilityChange = this.onVisibilityChange.bind(this);
|
|
39
|
-
}
|
|
40
|
-
// ─── Public API ────────────────────────────────────────────
|
|
41
|
-
start(config = {}, callbacks) {
|
|
42
|
-
if (this.running) {
|
|
43
|
-
this.stop();
|
|
44
|
-
}
|
|
45
|
-
this.config = { ...DEFAULT_SESSION_CONFIG, ...config };
|
|
46
|
-
this.callbacks = callbacks;
|
|
47
|
-
this.running = true;
|
|
48
|
-
this.warningActive = false;
|
|
49
|
-
this.lastActivityTime = Date.now();
|
|
50
|
-
if (!this.config.isAutoLogoutEnabled) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
ACTIVITY_EVENTS.forEach((event) => {
|
|
54
|
-
document.addEventListener(event, this.boundOnActivity, {
|
|
55
|
-
capture: true,
|
|
56
|
-
passive: true,
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
document.addEventListener('visibilitychange', this.boundOnVisibilityChange);
|
|
60
|
-
this.startHeartbeat();
|
|
61
|
-
this.callbacks?.onHeartbeat();
|
|
62
|
-
this.idleCheckInterval = setInterval(() => this.checkIdle(), 1000);
|
|
63
|
-
}
|
|
64
|
-
stop() {
|
|
65
|
-
this.running = false;
|
|
66
|
-
this.warningActive = false;
|
|
67
|
-
ACTIVITY_EVENTS.forEach((event) => {
|
|
68
|
-
document.removeEventListener(event, this.boundOnActivity, {
|
|
69
|
-
capture: true,
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
document.removeEventListener('visibilitychange', this.boundOnVisibilityChange);
|
|
73
|
-
this.clearTimer('idleCheckInterval');
|
|
74
|
-
this.clearTimer('heartbeatInterval');
|
|
75
|
-
this.clearTimer('countdownInterval');
|
|
76
|
-
this.clearTimer('activityDebounceTimer');
|
|
77
|
-
}
|
|
78
|
-
continueSession() {
|
|
79
|
-
if (!this.running) {
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
this.warningActive = false;
|
|
83
|
-
this.secondsRemaining = 0;
|
|
84
|
-
this.lastActivityTime = Date.now();
|
|
85
|
-
this.clearTimer('countdownInterval');
|
|
86
|
-
this.callbacks?.onHeartbeat();
|
|
87
|
-
this.callbacks?.onSessionExtended();
|
|
88
|
-
this.startHeartbeat();
|
|
89
|
-
}
|
|
90
|
-
isWarningActive() {
|
|
91
|
-
return this.warningActive;
|
|
92
|
-
}
|
|
93
|
-
getSecondsRemaining() {
|
|
94
|
-
return this.secondsRemaining;
|
|
95
|
-
}
|
|
96
|
-
// ─── Private ───────────────────────────────────────────────
|
|
97
|
-
onActivity() {
|
|
98
|
-
if (!this.running || this.warningActive) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (this.activityDebounceTimer != null) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
this.lastActivityTime = Date.now();
|
|
105
|
-
this.activityDebounceTimer = setTimeout(() => {
|
|
106
|
-
this.activityDebounceTimer = null;
|
|
107
|
-
}, ACTIVITY_DEBOUNCE_MS);
|
|
108
|
-
}
|
|
109
|
-
onVisibilityChange() {
|
|
110
|
-
if (!this.running) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (document.visibilityState === 'visible') {
|
|
114
|
-
this.lastActivityTime = Date.now();
|
|
115
|
-
this.checkIdle();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
checkIdle() {
|
|
119
|
-
if (!this.running || this.warningActive) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const idleMs = Date.now() - this.lastActivityTime;
|
|
123
|
-
const idleTimeoutMs = this.config.idleTimeoutMinutes * 60 * 1000;
|
|
124
|
-
if (idleMs >= idleTimeoutMs) {
|
|
125
|
-
this.startWarning();
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
startWarning() {
|
|
129
|
-
this.warningActive = true;
|
|
130
|
-
this.secondsRemaining = this.config.warningDurationSeconds;
|
|
131
|
-
this.clearTimer('heartbeatInterval');
|
|
132
|
-
this.callbacks?.onWarningStart(this.secondsRemaining);
|
|
133
|
-
this.countdownInterval = setInterval(() => {
|
|
134
|
-
this.secondsRemaining -= 1;
|
|
135
|
-
if (this.secondsRemaining <= 0) {
|
|
136
|
-
this.clearTimer('countdownInterval');
|
|
137
|
-
this.warningActive = false;
|
|
138
|
-
this.callbacks?.onAutoLogout();
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
this.callbacks?.onWarningTick(this.secondsRemaining);
|
|
142
|
-
}
|
|
143
|
-
}, 1000);
|
|
144
|
-
}
|
|
145
|
-
startHeartbeat() {
|
|
146
|
-
this.clearTimer('heartbeatInterval');
|
|
147
|
-
const intervalMs = this.config.heartbeatIntervalMinutes * 60 * 1000;
|
|
148
|
-
this.heartbeatInterval = setInterval(() => {
|
|
149
|
-
if (!this.running || this.warningActive) {
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
this.callbacks?.onHeartbeat();
|
|
153
|
-
}, intervalMs);
|
|
154
|
-
}
|
|
155
|
-
clearTimer(name) {
|
|
156
|
-
const timer = this[name];
|
|
157
|
-
if (timer != null) {
|
|
158
|
-
if (name === 'activityDebounceTimer') {
|
|
159
|
-
clearTimeout(timer);
|
|
160
|
-
}
|
|
161
|
-
else {
|
|
162
|
-
clearInterval(timer);
|
|
163
|
-
}
|
|
164
|
-
this[name] = null;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { of } from 'rxjs';
|
|
2
|
-
import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
-
import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
|
|
4
|
-
import { sendSessionHeartbeat, sessionHeartbeatFailure, sessionHeartbeatSuccess, } from '../tenantReducer';
|
|
5
|
-
export const sessionHeartbeatEpic = (actions$, _, zeniAPI) => actions$.pipe(filter(sendSessionHeartbeat.match), switchMap(() => zeniAPI
|
|
6
|
-
.postAndGetJSON(`${zeniAPI.apiEndPoints.authMicroServiceBaseUrl}/1.0/sessions/heartbeat`)
|
|
7
|
-
.pipe(mergeMap((response) => {
|
|
8
|
-
if (isSuccessResponse(response)) {
|
|
9
|
-
return of(sessionHeartbeatSuccess(response.data?.expiry ?? ''));
|
|
10
|
-
}
|
|
11
|
-
return of(sessionHeartbeatFailure(response.status));
|
|
12
|
-
}), catchError((error) => of(sessionHeartbeatFailure(createZeniAPIStatus('Heartbeat failed', 'Session heartbeat API call errored: ' + JSON.stringify(error))))))));
|