@trevonistrevon/pi-loop 0.4.10 → 0.5.0
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/README.md +11 -1
- package/dist/coordinator.d.ts +35 -0
- package/dist/coordinator.js +56 -0
- package/dist/goal-types.d.ts +78 -0
- package/dist/goal-types.js +1 -0
- package/dist/goal-verifier.d.ts +20 -0
- package/dist/goal-verifier.js +198 -0
- package/dist/index.js +182 -40
- package/dist/loop-reducer.d.ts +63 -0
- package/dist/loop-reducer.js +67 -0
- package/dist/monitor-completion-coordinator.d.ts +10 -0
- package/dist/monitor-completion-coordinator.js +13 -0
- package/dist/monitor-manager.d.ts +2 -0
- package/dist/monitor-manager.js +107 -29
- package/dist/monitor-reducer.d.ts +82 -0
- package/dist/monitor-reducer.js +69 -0
- package/dist/notification-reducer.d.ts +81 -0
- package/dist/notification-reducer.js +65 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +118 -44
- package/dist/task-backlog-coordinator.d.ts +12 -0
- package/dist/task-backlog-coordinator.js +22 -0
- package/dist/task-reducer.d.ts +66 -0
- package/dist/task-reducer.js +76 -0
- package/dist/task-store.d.ts +2 -0
- package/dist/task-store.js +82 -30
- package/docs/architecture/goal-state-schema.md +505 -0
- package/docs/architecture/state-machine-migration.md +546 -0
- package/docs/architecture/state-machine-reducer-event-model.md +823 -0
- package/docs/architecture/state-machine-test-matrix.md +249 -0
- package/docs/architecture/state-machine-transition-map.md +436 -0
- package/package.json +1 -1
- package/src/coordinator.ts +115 -0
- package/src/goal-types.ts +99 -0
- package/src/goal-verifier.ts +241 -0
- package/src/index.ts +209 -39
- package/src/loop-reducer.ts +148 -0
- package/src/monitor-completion-coordinator.ts +24 -0
- package/src/monitor-manager.ts +115 -27
- package/src/monitor-reducer.ts +166 -0
- package/src/notification-reducer.ts +155 -0
- package/src/store.ts +119 -43
- package/src/task-backlog-coordinator.ts +32 -0
- package/src/task-reducer.ts +152 -0
- package/src/task-store.ts +84 -27
package/src/monitor-manager.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import {
|
|
5
|
+
type MonitorReducerEvent,
|
|
6
|
+
type MonitorReducerState,
|
|
7
|
+
reduceMonitorState,
|
|
8
|
+
} from "./monitor-reducer.js";
|
|
4
9
|
import type { MonitorEntry, MonitorProcess } from "./types.js";
|
|
5
10
|
|
|
6
11
|
export class MonitorManager {
|
|
@@ -9,18 +14,45 @@ export class MonitorManager {
|
|
|
9
14
|
|
|
10
15
|
constructor(private pi: ExtensionAPI) {}
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
timeout,
|
|
19
|
-
status: "running",
|
|
20
|
-
startedAt: Date.now(),
|
|
21
|
-
outputLines: 0,
|
|
22
|
-
outputBuffer: [],
|
|
17
|
+
private toReducerState(): MonitorReducerState {
|
|
18
|
+
return {
|
|
19
|
+
nextId: this.nextId,
|
|
20
|
+
monitorsById: Object.fromEntries(
|
|
21
|
+
Array.from(this.processes.entries()).map(([id, process]) => [id, process.entry]),
|
|
22
|
+
),
|
|
23
23
|
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private applyReducerEvent(event: MonitorReducerEvent) {
|
|
27
|
+
const result = reduceMonitorState(this.toReducerState(), event);
|
|
28
|
+
this.nextId = result.state.nextId;
|
|
29
|
+
for (const [id, process] of [...this.processes.entries()]) {
|
|
30
|
+
const updated = result.state.monitorsById[id];
|
|
31
|
+
if (!updated) {
|
|
32
|
+
this.processes.delete(id);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
process.entry = updated;
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
create(command: string, description?: string, timeout = 300000): MonitorEntry {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const result = reduceMonitorState(this.toReducerState(), {
|
|
43
|
+
type: "MONITOR_CREATED",
|
|
44
|
+
at: now,
|
|
45
|
+
source: "tool",
|
|
46
|
+
entityType: "monitor",
|
|
47
|
+
payload: {
|
|
48
|
+
command,
|
|
49
|
+
description,
|
|
50
|
+
timeout,
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
this.nextId = result.state.nextId;
|
|
54
|
+
const id = String(this.nextId - 1);
|
|
55
|
+
const entry = result.state.monitorsById[id]!;
|
|
24
56
|
|
|
25
57
|
const abortController = new AbortController();
|
|
26
58
|
const child = spawn("sh", ["-c", command], {
|
|
@@ -42,8 +74,14 @@ export class MonitorManager {
|
|
|
42
74
|
const lines = data.toString().split("\n");
|
|
43
75
|
for (const line of lines) {
|
|
44
76
|
if (line.length === 0) continue;
|
|
45
|
-
|
|
46
|
-
|
|
77
|
+
this.applyReducerEvent({
|
|
78
|
+
type: "MONITOR_OUTPUT",
|
|
79
|
+
at: Date.now(),
|
|
80
|
+
source: "monitor",
|
|
81
|
+
entityType: "monitor",
|
|
82
|
+
entityId: id,
|
|
83
|
+
payload: { id, line },
|
|
84
|
+
});
|
|
47
85
|
this.pi.events.emit("monitor:output", {
|
|
48
86
|
monitorId: id,
|
|
49
87
|
line,
|
|
@@ -56,8 +94,14 @@ export class MonitorManager {
|
|
|
56
94
|
const lines = data.toString().split("\n");
|
|
57
95
|
for (const line of lines) {
|
|
58
96
|
if (line.length === 0) continue;
|
|
59
|
-
|
|
60
|
-
|
|
97
|
+
this.applyReducerEvent({
|
|
98
|
+
type: "MONITOR_OUTPUT",
|
|
99
|
+
at: Date.now(),
|
|
100
|
+
source: "monitor",
|
|
101
|
+
entityType: "monitor",
|
|
102
|
+
entityId: id,
|
|
103
|
+
payload: { id, line },
|
|
104
|
+
});
|
|
61
105
|
this.pi.events.emit("monitor:output", {
|
|
62
106
|
monitorId: id,
|
|
63
107
|
line,
|
|
@@ -67,13 +111,22 @@ export class MonitorManager {
|
|
|
67
111
|
});
|
|
68
112
|
|
|
69
113
|
const finish = (code: number | null, status: "completed" | "error") => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
this.applyReducerEvent({
|
|
115
|
+
type: status === "completed" ? "MONITOR_COMPLETED" : "MONITOR_ERRORED",
|
|
116
|
+
at: Date.now(),
|
|
117
|
+
source: "monitor",
|
|
118
|
+
entityType: "monitor",
|
|
119
|
+
entityId: id,
|
|
120
|
+
payload: {
|
|
121
|
+
id,
|
|
122
|
+
exitCode: code ?? undefined,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const current = this.get(id)!;
|
|
73
126
|
this.pi.events.emit(status === "completed" ? "monitor:done" : "monitor:error", {
|
|
74
127
|
monitorId: id,
|
|
75
128
|
exitCode: code,
|
|
76
|
-
outputLines:
|
|
129
|
+
outputLines: current.outputLines,
|
|
77
130
|
});
|
|
78
131
|
if (status === "completed") {
|
|
79
132
|
for (const callback of bp.completionCallbacks) callback();
|
|
@@ -83,19 +136,37 @@ export class MonitorManager {
|
|
|
83
136
|
bp.waiters = [];
|
|
84
137
|
// Remove completed/errored monitors after a brief delay so tool
|
|
85
138
|
// consumers have time to read the final state via MonitorList.
|
|
86
|
-
setTimeout(() => {
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
this.applyReducerEvent({
|
|
141
|
+
type: "MONITOR_PRUNED",
|
|
142
|
+
at: Date.now(),
|
|
143
|
+
source: "system",
|
|
144
|
+
entityType: "monitor",
|
|
145
|
+
entityId: id,
|
|
146
|
+
payload: { id },
|
|
147
|
+
});
|
|
148
|
+
}, 30000);
|
|
87
149
|
};
|
|
88
150
|
|
|
89
151
|
child.on("close", (code) => {
|
|
90
|
-
if (entry.status === "running") {
|
|
152
|
+
if (bp.entry.status === "running") {
|
|
91
153
|
finish(code, code === 0 ? "completed" : "error");
|
|
92
154
|
}
|
|
93
155
|
});
|
|
94
156
|
|
|
95
157
|
child.on("error", (err) => {
|
|
96
|
-
if (entry.status === "running") {
|
|
97
|
-
|
|
98
|
-
|
|
158
|
+
if (bp.entry.status === "running") {
|
|
159
|
+
this.applyReducerEvent({
|
|
160
|
+
type: "MONITOR_ERRORED",
|
|
161
|
+
at: Date.now(),
|
|
162
|
+
source: "monitor",
|
|
163
|
+
entityType: "monitor",
|
|
164
|
+
entityId: id,
|
|
165
|
+
payload: {
|
|
166
|
+
id,
|
|
167
|
+
error: err.message,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
99
170
|
this.pi.events.emit("monitor:error", {
|
|
100
171
|
monitorId: id,
|
|
101
172
|
error: err.message,
|
|
@@ -108,7 +179,7 @@ export class MonitorManager {
|
|
|
108
179
|
|
|
109
180
|
if (timeout > 0) {
|
|
110
181
|
setTimeout(() => {
|
|
111
|
-
if (entry.status === "running") {
|
|
182
|
+
if (bp.entry.status === "running") {
|
|
112
183
|
this.stop(id);
|
|
113
184
|
}
|
|
114
185
|
}, timeout);
|
|
@@ -133,7 +204,17 @@ export class MonitorManager {
|
|
|
133
204
|
const bp = this.processes.get(id);
|
|
134
205
|
if (!bp || bp.entry.status !== "running") return false;
|
|
135
206
|
|
|
136
|
-
|
|
207
|
+
this.applyReducerEvent({
|
|
208
|
+
type: "MONITOR_STOPPED",
|
|
209
|
+
at: Date.now(),
|
|
210
|
+
source: "tool",
|
|
211
|
+
entityType: "monitor",
|
|
212
|
+
entityId: id,
|
|
213
|
+
payload: {
|
|
214
|
+
id,
|
|
215
|
+
reason: "manual",
|
|
216
|
+
},
|
|
217
|
+
});
|
|
137
218
|
bp.proc.kill("SIGTERM");
|
|
138
219
|
|
|
139
220
|
await new Promise<void>((resolve) => {
|
|
@@ -148,7 +229,6 @@ export class MonitorManager {
|
|
|
148
229
|
});
|
|
149
230
|
});
|
|
150
231
|
|
|
151
|
-
bp.entry.completedAt = Date.now();
|
|
152
232
|
bp.completionCallbacks = [];
|
|
153
233
|
for (const resolve of bp.waiters) resolve();
|
|
154
234
|
bp.waiters = [];
|
|
@@ -163,6 +243,14 @@ export class MonitorManager {
|
|
|
163
243
|
return true;
|
|
164
244
|
}
|
|
165
245
|
if (bp.entry.status !== "running") return false;
|
|
246
|
+
this.applyReducerEvent({
|
|
247
|
+
type: "MONITOR_ONDONE_REGISTERED",
|
|
248
|
+
at: Date.now(),
|
|
249
|
+
source: "tool",
|
|
250
|
+
entityType: "monitor",
|
|
251
|
+
entityId: id,
|
|
252
|
+
payload: { id },
|
|
253
|
+
});
|
|
166
254
|
bp.completionCallbacks.push(callback);
|
|
167
255
|
return true;
|
|
168
256
|
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { MonitorEntry } from "./types.js";
|
|
2
|
+
|
|
3
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
4
|
+
|
|
5
|
+
export interface MonitorReducerEntry extends MonitorEntry {
|
|
6
|
+
onDoneRegistered?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MonitorReducerState {
|
|
10
|
+
nextId: number;
|
|
11
|
+
monitorsById: Record<string, MonitorReducerEntry>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type MonitorReducerEvent =
|
|
15
|
+
| {
|
|
16
|
+
type: "MONITOR_CREATED";
|
|
17
|
+
at: number;
|
|
18
|
+
source: ReducerSource;
|
|
19
|
+
entityType?: "monitor";
|
|
20
|
+
entityId?: string;
|
|
21
|
+
payload: {
|
|
22
|
+
command: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
timeout: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: "MONITOR_OUTPUT";
|
|
29
|
+
at: number;
|
|
30
|
+
source: ReducerSource;
|
|
31
|
+
entityType?: "monitor";
|
|
32
|
+
entityId?: string;
|
|
33
|
+
payload: {
|
|
34
|
+
id: string;
|
|
35
|
+
line: string;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
type: "MONITOR_COMPLETED" | "MONITOR_ERRORED";
|
|
40
|
+
at: number;
|
|
41
|
+
source: ReducerSource;
|
|
42
|
+
entityType?: "monitor";
|
|
43
|
+
entityId?: string;
|
|
44
|
+
payload: {
|
|
45
|
+
id: string;
|
|
46
|
+
exitCode?: number;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
type: "MONITOR_STOPPED";
|
|
52
|
+
at: number;
|
|
53
|
+
source: ReducerSource;
|
|
54
|
+
entityType?: "monitor";
|
|
55
|
+
entityId?: string;
|
|
56
|
+
payload: {
|
|
57
|
+
id: string;
|
|
58
|
+
reason: "manual" | "timeout";
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
type: "MONITOR_PRUNED" | "MONITOR_ONDONE_REGISTERED";
|
|
63
|
+
at: number;
|
|
64
|
+
source: ReducerSource;
|
|
65
|
+
entityType?: "monitor";
|
|
66
|
+
entityId?: string;
|
|
67
|
+
payload: {
|
|
68
|
+
id: string;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type MonitorReducerEffect =
|
|
73
|
+
| {
|
|
74
|
+
type: "PERSIST_MONITOR";
|
|
75
|
+
entityType: "monitor";
|
|
76
|
+
entityId: string;
|
|
77
|
+
payload: { monitor: MonitorReducerEntry };
|
|
78
|
+
}
|
|
79
|
+
| {
|
|
80
|
+
type: "DELETE_MONITOR";
|
|
81
|
+
entityType: "monitor";
|
|
82
|
+
entityId: string;
|
|
83
|
+
payload: { id: string };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export interface MonitorReduceResult {
|
|
87
|
+
state: MonitorReducerState;
|
|
88
|
+
effects: MonitorReducerEffect[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function cloneState(state: MonitorReducerState): MonitorReducerState {
|
|
92
|
+
return {
|
|
93
|
+
nextId: state.nextId,
|
|
94
|
+
monitorsById: { ...state.monitorsById },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function reduceMonitorState(state: MonitorReducerState, event: MonitorReducerEvent): MonitorReduceResult {
|
|
99
|
+
if (event.type === "MONITOR_CREATED") {
|
|
100
|
+
const next = cloneState(state);
|
|
101
|
+
const id = String(next.nextId++);
|
|
102
|
+
const monitor: MonitorReducerEntry = {
|
|
103
|
+
id,
|
|
104
|
+
command: event.payload.command,
|
|
105
|
+
description: event.payload.description,
|
|
106
|
+
timeout: event.payload.timeout,
|
|
107
|
+
status: "running",
|
|
108
|
+
startedAt: event.at,
|
|
109
|
+
outputLines: 0,
|
|
110
|
+
outputBuffer: [],
|
|
111
|
+
};
|
|
112
|
+
next.monitorsById[id] = monitor;
|
|
113
|
+
return {
|
|
114
|
+
state: next,
|
|
115
|
+
effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const id = event.payload.id;
|
|
120
|
+
const current = state.monitorsById[id];
|
|
121
|
+
if (!current) return { state, effects: [] };
|
|
122
|
+
|
|
123
|
+
if (event.type === "MONITOR_PRUNED") {
|
|
124
|
+
const next = cloneState(state);
|
|
125
|
+
delete next.monitorsById[id];
|
|
126
|
+
return {
|
|
127
|
+
state: next,
|
|
128
|
+
effects: [{ type: "DELETE_MONITOR", entityType: "monitor", entityId: id, payload: { id } }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const next = cloneState(state);
|
|
133
|
+
const monitor: MonitorReducerEntry = { ...current };
|
|
134
|
+
|
|
135
|
+
if (event.type === "MONITOR_OUTPUT") {
|
|
136
|
+
monitor.outputLines++;
|
|
137
|
+
if (monitor.outputBuffer.length < 200) monitor.outputBuffer = [...monitor.outputBuffer, event.payload.line];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (event.type === "MONITOR_COMPLETED") {
|
|
141
|
+
monitor.status = "completed";
|
|
142
|
+
monitor.exitCode = event.payload.exitCode;
|
|
143
|
+
monitor.completedAt = event.at;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (event.type === "MONITOR_ERRORED") {
|
|
147
|
+
monitor.status = "error";
|
|
148
|
+
if (event.payload.exitCode !== undefined) monitor.exitCode = event.payload.exitCode;
|
|
149
|
+
monitor.completedAt = event.at;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (event.type === "MONITOR_STOPPED") {
|
|
153
|
+
monitor.status = "stopped";
|
|
154
|
+
monitor.completedAt = event.at;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (event.type === "MONITOR_ONDONE_REGISTERED") {
|
|
158
|
+
monitor.onDoneRegistered = true;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
next.monitorsById[id] = monitor;
|
|
162
|
+
return {
|
|
163
|
+
state: next,
|
|
164
|
+
effects: [{ type: "PERSIST_MONITOR", entityType: "monitor", entityId: id, payload: { monitor } }],
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
type ReducerSource = "tool" | "command" | "scheduler" | "eventbus" | "monitor" | "session" | "coordinator" | "system";
|
|
2
|
+
|
|
3
|
+
export interface ReducerNotification {
|
|
4
|
+
key: string;
|
|
5
|
+
loopId: string;
|
|
6
|
+
message: string;
|
|
7
|
+
timestamp: number;
|
|
8
|
+
trigger: unknown;
|
|
9
|
+
recurring?: boolean;
|
|
10
|
+
autoTask?: boolean;
|
|
11
|
+
readOnly?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface NotificationReducerState {
|
|
15
|
+
notificationsByKey: Record<string, ReducerNotification>;
|
|
16
|
+
agentRunning: boolean;
|
|
17
|
+
hasPendingMessages: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type NotificationReducerEvent =
|
|
21
|
+
| {
|
|
22
|
+
type: "NOTIFICATION_QUEUED";
|
|
23
|
+
at: number;
|
|
24
|
+
source: ReducerSource;
|
|
25
|
+
entityType?: "notification";
|
|
26
|
+
entityId?: string;
|
|
27
|
+
payload: { notification: ReducerNotification };
|
|
28
|
+
}
|
|
29
|
+
| {
|
|
30
|
+
type: "NOTIFICATION_DROPPED";
|
|
31
|
+
at: number;
|
|
32
|
+
source: ReducerSource;
|
|
33
|
+
entityType?: "notification";
|
|
34
|
+
entityId?: string;
|
|
35
|
+
payload: {
|
|
36
|
+
key: string;
|
|
37
|
+
reason: "zero_pending_tasks" | "session_switch" | "session_shutdown" | "superseded";
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
type: "NOTIFICATION_CLEARED";
|
|
42
|
+
at: number;
|
|
43
|
+
source: ReducerSource;
|
|
44
|
+
entityType?: "notification";
|
|
45
|
+
entityId?: string;
|
|
46
|
+
payload: { reason: "session_switch" | "session_shutdown" };
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
type: "NOTIFICATION_FLUSH_REQUESTED";
|
|
50
|
+
at: number;
|
|
51
|
+
source: ReducerSource;
|
|
52
|
+
entityType?: "notification";
|
|
53
|
+
entityId?: string;
|
|
54
|
+
payload: { ignorePendingMessages?: boolean };
|
|
55
|
+
}
|
|
56
|
+
| {
|
|
57
|
+
type: "NOTIFICATION_RUNTIME_UPDATED";
|
|
58
|
+
at: number;
|
|
59
|
+
source: ReducerSource;
|
|
60
|
+
entityType?: "notification";
|
|
61
|
+
entityId?: string;
|
|
62
|
+
payload: { agentRunning: boolean; hasPendingMessages: boolean };
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type NotificationReducerEffect =
|
|
66
|
+
| {
|
|
67
|
+
type: "REQUEST_NOTIFICATION_FLUSH";
|
|
68
|
+
payload: Record<string, never>;
|
|
69
|
+
}
|
|
70
|
+
| {
|
|
71
|
+
type: "DELIVER_NOTIFICATION";
|
|
72
|
+
entityType: "notification";
|
|
73
|
+
entityId: string;
|
|
74
|
+
payload: { notification: ReducerNotification };
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface NotificationReduceResult {
|
|
78
|
+
state: NotificationReducerState;
|
|
79
|
+
effects: NotificationReducerEffect[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function cloneState(state: NotificationReducerState): NotificationReducerState {
|
|
83
|
+
return {
|
|
84
|
+
notificationsByKey: { ...state.notificationsByKey },
|
|
85
|
+
agentRunning: state.agentRunning,
|
|
86
|
+
hasPendingMessages: state.hasPendingMessages,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function reduceNotificationState(
|
|
91
|
+
state: NotificationReducerState,
|
|
92
|
+
event: NotificationReducerEvent,
|
|
93
|
+
): NotificationReduceResult {
|
|
94
|
+
if (event.type === "NOTIFICATION_QUEUED") {
|
|
95
|
+
const next = cloneState(state);
|
|
96
|
+
next.notificationsByKey[event.payload.notification.key] = event.payload.notification;
|
|
97
|
+
return {
|
|
98
|
+
state: next,
|
|
99
|
+
effects: [{ type: "REQUEST_NOTIFICATION_FLUSH", payload: {} }],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (event.type === "NOTIFICATION_DROPPED") {
|
|
104
|
+
const next = cloneState(state);
|
|
105
|
+
delete next.notificationsByKey[event.payload.key];
|
|
106
|
+
return { state: next, effects: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (event.type === "NOTIFICATION_CLEARED") {
|
|
110
|
+
return {
|
|
111
|
+
state: {
|
|
112
|
+
...state,
|
|
113
|
+
notificationsByKey: {},
|
|
114
|
+
},
|
|
115
|
+
effects: [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.type === "NOTIFICATION_RUNTIME_UPDATED") {
|
|
120
|
+
return {
|
|
121
|
+
state: {
|
|
122
|
+
...state,
|
|
123
|
+
agentRunning: event.payload.agentRunning,
|
|
124
|
+
hasPendingMessages: event.payload.hasPendingMessages,
|
|
125
|
+
},
|
|
126
|
+
effects: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (event.type === "NOTIFICATION_FLUSH_REQUESTED") {
|
|
131
|
+
if (state.agentRunning) return { state, effects: [] };
|
|
132
|
+
if (!event.payload.ignorePendingMessages && state.hasPendingMessages) {
|
|
133
|
+
return { state, effects: [] };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const queued = Object.values(state.notificationsByKey)
|
|
137
|
+
.sort((left, right) => left.timestamp - right.timestamp);
|
|
138
|
+
const nextNotification = queued[0];
|
|
139
|
+
if (!nextNotification) return { state, effects: [] };
|
|
140
|
+
|
|
141
|
+
const next = cloneState(state);
|
|
142
|
+
delete next.notificationsByKey[nextNotification.key];
|
|
143
|
+
return {
|
|
144
|
+
state: next,
|
|
145
|
+
effects: [{
|
|
146
|
+
type: "DELIVER_NOTIFICATION",
|
|
147
|
+
entityType: "notification",
|
|
148
|
+
entityId: nextNotification.key,
|
|
149
|
+
payload: { notification: nextNotification },
|
|
150
|
+
}],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { state, effects: [] };
|
|
155
|
+
}
|