effective-progress 0.1.0 → 0.1.3
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 +127 -17
- package/package.json +17 -10
- package/src/api.ts +57 -76
- package/src/colors.ts +206 -0
- package/src/console.ts +3 -4
- package/src/index.ts +1 -0
- package/src/renderer.ts +131 -138
- package/src/runtime.ts +111 -51
- package/src/types.ts +33 -23
- package/src/utils.ts +20 -0
package/src/renderer.ts
CHANGED
|
@@ -1,58 +1,59 @@
|
|
|
1
|
-
import { Effect, Ref } from "effect";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import type { ProgressBarConfigShape } from "./types";
|
|
1
|
+
import { Effect, Ref, Schema } from "effect";
|
|
4
2
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "./
|
|
3
|
+
type CompiledProgressBarColors,
|
|
4
|
+
compileProgressBarColors,
|
|
5
|
+
ProgressBarColorsSchema,
|
|
6
|
+
} from "./colors";
|
|
7
|
+
import type { ProgressBarConfigShape, RendererConfigShape } from "./types";
|
|
8
|
+
import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
|
|
9
9
|
|
|
10
10
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
11
11
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
12
12
|
const CLEAR_LINE = "\x1b[2K";
|
|
13
13
|
const MOVE_UP_ONE = "\x1b[1A";
|
|
14
|
-
const
|
|
14
|
+
const encodeProgressBarColorsKey = Schema.encodeSync(Schema.parseJson(ProgressBarColorsSchema));
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const renderDeterminate = (units: DeterminateTaskUnits, config: ProgressBarConfigShape): string => {
|
|
16
|
+
const renderDeterminate = (
|
|
17
|
+
units: DeterminateTaskUnits,
|
|
18
|
+
progressbar: ProgressBarConfigShape,
|
|
19
|
+
colors: CompiledProgressBarColors,
|
|
20
|
+
): string => {
|
|
22
21
|
const safeTotal = units.total <= 0 ? 1 : units.total;
|
|
23
22
|
const ratio = Math.min(1, Math.max(0, units.completed / safeTotal));
|
|
24
|
-
const filled = Math.round(ratio *
|
|
25
|
-
const bar = `${
|
|
23
|
+
const filled = Math.round(ratio * progressbar.barWidth);
|
|
24
|
+
const bar = `${colors.fill(progressbar.fillChar.repeat(filled))}${colors.empty(progressbar.emptyChar.repeat(progressbar.barWidth - filled))}`;
|
|
26
25
|
const percent = String(Math.round(ratio * 100)).padStart(3, " ");
|
|
27
|
-
return `${
|
|
26
|
+
return `${colors.brackets(progressbar.leftBracket)}${bar}${colors.brackets(progressbar.rightBracket)} ${units.completed}/${units.total} ${colors.percent(percent + "%")}`;
|
|
28
27
|
};
|
|
29
28
|
|
|
30
29
|
const buildTaskLine = (
|
|
31
30
|
snapshot: TaskSnapshot,
|
|
32
31
|
depth: number,
|
|
33
32
|
tick: number,
|
|
34
|
-
|
|
33
|
+
colors: CompiledProgressBarColors,
|
|
35
34
|
): string => {
|
|
35
|
+
const progressbar = snapshot.progressbar;
|
|
36
36
|
const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
|
|
37
37
|
|
|
38
38
|
if (snapshot.status === "failed") {
|
|
39
|
-
return `${prefix}${
|
|
39
|
+
return `${prefix}${colors.failed("[failed]")}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (snapshot.status === "done") {
|
|
43
43
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
44
|
-
return `${prefix}${
|
|
44
|
+
return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}`;
|
|
45
45
|
}
|
|
46
|
-
return `${prefix}${
|
|
46
|
+
return `${prefix}${colors.done("[done]")}`;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
50
|
-
return prefix + renderDeterminate(snapshot.units,
|
|
50
|
+
return prefix + renderDeterminate(snapshot.units, progressbar, colors);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
const frames =
|
|
53
|
+
const frames = progressbar.spinnerFrames;
|
|
54
54
|
const frameIndex = (snapshot.units.spinnerFrame + tick) % frames.length;
|
|
55
|
-
|
|
55
|
+
const frame = frames[frameIndex] ?? frames[0]!;
|
|
56
|
+
return `${prefix}${colors.spinner(frame)}`;
|
|
56
57
|
};
|
|
57
58
|
|
|
58
59
|
const orderTasksForRender = (
|
|
@@ -80,86 +81,127 @@ const orderTasksForRender = (
|
|
|
80
81
|
|
|
81
82
|
export const runProgressServiceRenderer = (
|
|
82
83
|
tasksRef: Ref.Ref<Map<TaskId, TaskSnapshot>>,
|
|
83
|
-
logsRef: Ref.Ref<ReadonlyArray<
|
|
84
|
+
logsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
85
|
+
pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
84
86
|
dirtyRef: Ref.Ref<boolean>,
|
|
85
|
-
|
|
87
|
+
rendererConfig: RendererConfigShape,
|
|
88
|
+
maxRetainedLogLines: number,
|
|
89
|
+
rendererLatch: Effect.Latch,
|
|
86
90
|
) => {
|
|
87
|
-
const isTTY =
|
|
91
|
+
const isTTY = rendererConfig.isTTY;
|
|
92
|
+
const retainLogHistory = maxRetainedLogLines > 0;
|
|
93
|
+
const colorCache = new Map<string, CompiledProgressBarColors>();
|
|
88
94
|
let previousLineCount = 0;
|
|
89
|
-
let
|
|
95
|
+
let previousTaskLineCount = 0;
|
|
90
96
|
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
91
97
|
let tick = 0;
|
|
98
|
+
let rendererActive = false;
|
|
92
99
|
let teardownInput: (() => void) | undefined;
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
const getCompiledColors = (progressbar: ProgressBarConfigShape): CompiledProgressBarColors => {
|
|
102
|
+
const key = encodeProgressBarColorsKey(progressbar.colors);
|
|
103
|
+
const cached = colorCache.get(key);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const compiled = compileProgressBarColors(progressbar.colors);
|
|
109
|
+
colorCache.set(key, compiled);
|
|
110
|
+
return compiled;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const clearTTYLines = (lineCount: number) => {
|
|
114
|
+
if (lineCount <= 0) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let output = "\r" + CLEAR_LINE;
|
|
119
|
+
for (let i = 1; i < lineCount; i++) {
|
|
120
|
+
output += MOVE_UP_ONE + CLEAR_LINE;
|
|
121
|
+
}
|
|
122
|
+
process.stderr.write(output + "\r");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const renderNonTTYTaskUpdates = (
|
|
126
|
+
ordered: ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }>,
|
|
127
|
+
taskLines: ReadonlyArray<string>,
|
|
128
|
+
) => {
|
|
129
|
+
const nextTaskSignatureById = new Map<number, string>();
|
|
130
|
+
const changedTaskLines: Array<string> = [];
|
|
131
|
+
const nonTtyUpdateStep = Math.max(1, Math.floor(rendererConfig.nonTtyUpdateStep));
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
134
|
+
const taskId = ordered[i]!.snapshot.id as number;
|
|
135
|
+
const snapshot = ordered[i]!.snapshot;
|
|
136
|
+
const line = taskLines[i]!;
|
|
137
|
+
const signature =
|
|
138
|
+
snapshot.units._tag === "DeterminateTaskUnits"
|
|
139
|
+
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
140
|
+
: `${snapshot.status}:${snapshot.description}`;
|
|
141
|
+
|
|
142
|
+
nextTaskSignatureById.set(taskId, signature);
|
|
143
|
+
if (nonTTYTaskSignatureById.get(taskId) !== signature) {
|
|
144
|
+
changedTaskLines.push(line);
|
|
99
145
|
}
|
|
100
|
-
|
|
101
|
-
previousLineCount = 0;
|
|
102
|
-
};
|
|
146
|
+
}
|
|
103
147
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
|
|
108
|
-
(task) => !(task.transient && task.status !== "running"),
|
|
109
|
-
);
|
|
110
|
-
const ordered = orderTasksForRender(snapshots);
|
|
111
|
-
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
112
|
-
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
113
|
-
const lineTick = isTTY ? frameTick : 0;
|
|
114
|
-
return buildTaskLine(snapshot, depth, lineTick, config);
|
|
115
|
-
});
|
|
116
|
-
const logLines = logs.map((log) => log.message);
|
|
117
|
-
const lines = [...logLines, ...taskLines];
|
|
148
|
+
if (changedTaskLines.length > 0) {
|
|
149
|
+
process.stderr.write(changedTaskLines.join("\n") + "\n");
|
|
150
|
+
}
|
|
118
151
|
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
nonTTYTaskSignatureById = nextTaskSignatureById;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const renderFrame = (mode: "tick" | "final") =>
|
|
156
|
+
Effect.gen(function* () {
|
|
157
|
+
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
158
|
+
const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
|
|
159
|
+
(task) => !(task.transient && task.status !== "running"),
|
|
160
|
+
);
|
|
161
|
+
const ordered = orderTasksForRender(snapshots);
|
|
162
|
+
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
163
|
+
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
164
|
+
const lineTick = isTTY ? frameTick : 0;
|
|
165
|
+
return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.progressbar));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (isTTY) {
|
|
169
|
+
if (retainLogHistory) {
|
|
170
|
+
const historyLogs = yield* Ref.get(logsRef);
|
|
171
|
+
const lines = [...historyLogs, ...taskLines];
|
|
172
|
+
clearTTYLines(previousLineCount);
|
|
121
173
|
if (lines.length > 0) {
|
|
122
174
|
process.stderr.write(lines.join("\n"));
|
|
123
|
-
previousLineCount = lines.length;
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
const appendedLogs = logs.filter((log) => log.id > nonTTYLastLogId);
|
|
127
|
-
if (appendedLogs.length > 0) {
|
|
128
|
-
process.stderr.write(appendedLogs.map((log) => log.message).join("\n") + "\n");
|
|
129
|
-
nonTTYLastLogId = appendedLogs[appendedLogs.length - 1]?.id ?? nonTTYLastLogId;
|
|
130
175
|
}
|
|
176
|
+
previousLineCount = lines.length;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
131
179
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
143
|
-
: `${snapshot.status}:${snapshot.description}`;
|
|
144
|
-
|
|
145
|
-
nextTaskSignatureById.set(taskId, signature);
|
|
146
|
-
if (nonTTYTaskSignatureById.get(taskId) !== signature) {
|
|
147
|
-
changedTaskLines.push(line);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
180
|
+
clearTTYLines(previousTaskLineCount);
|
|
181
|
+
if (drainedLogs.length > 0) {
|
|
182
|
+
process.stderr.write(drainedLogs.join("\n") + "\n");
|
|
183
|
+
}
|
|
184
|
+
if (taskLines.length > 0) {
|
|
185
|
+
process.stderr.write(taskLines.join("\n"));
|
|
186
|
+
}
|
|
187
|
+
previousTaskLineCount = taskLines.length;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
150
190
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
if (drainedLogs.length > 0) {
|
|
192
|
+
process.stderr.write(drainedLogs.join("\n") + "\n");
|
|
193
|
+
}
|
|
194
|
+
renderNonTTYTaskUpdates(ordered, taskLines);
|
|
195
|
+
});
|
|
154
196
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
197
|
+
return Effect.gen(function* () {
|
|
198
|
+
yield* rendererLatch.await;
|
|
199
|
+
rendererActive = true;
|
|
158
200
|
|
|
159
201
|
if (isTTY) {
|
|
160
202
|
process.stderr.write(HIDE_CURSOR);
|
|
161
203
|
|
|
162
|
-
if (
|
|
204
|
+
if (rendererConfig.disableUserInput && process.stdin.isTTY) {
|
|
163
205
|
const stdin = process.stdin;
|
|
164
206
|
const wasRaw = Boolean(stdin.isRaw);
|
|
165
207
|
stdin.resume();
|
|
@@ -200,66 +242,17 @@ export const runProgressServiceRenderer = (
|
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
tick += 1;
|
|
203
|
-
yield* Effect.sleep(
|
|
245
|
+
yield* Effect.sleep(Math.max(1, Math.floor(rendererConfig.renderIntervalMillis)));
|
|
204
246
|
}
|
|
205
247
|
}).pipe(
|
|
206
248
|
Effect.ensuring(
|
|
207
249
|
Effect.gen(function* () {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
(task) => !(task.transient && task.status !== "running"),
|
|
211
|
-
);
|
|
212
|
-
const ordered = orderTasksForRender(snapshots);
|
|
213
|
-
const taskLines = ordered.map(({ snapshot, depth }) =>
|
|
214
|
-
buildTaskLine(snapshot, depth, tick + 1, config),
|
|
215
|
-
);
|
|
216
|
-
const logLines = logs.map((log) => log.message);
|
|
217
|
-
const lines = [...logLines, ...taskLines];
|
|
218
|
-
|
|
219
|
-
if (isTTY) {
|
|
220
|
-
let output = "\r" + CLEAR_LINE;
|
|
221
|
-
for (let i = 1; i < previousLineCount; i++) {
|
|
222
|
-
output += MOVE_UP_ONE + CLEAR_LINE;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (lines.length > 0) {
|
|
226
|
-
process.stderr.write(output + "\r" + lines.join("\n"));
|
|
227
|
-
} else {
|
|
228
|
-
process.stderr.write(output + "\r");
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
const appendedLogs = logs.filter((log) => log.id > nonTTYLastLogId);
|
|
232
|
-
if (appendedLogs.length > 0) {
|
|
233
|
-
process.stderr.write(appendedLogs.map((log) => log.message).join("\n") + "\n");
|
|
234
|
-
nonTTYLastLogId = appendedLogs[appendedLogs.length - 1]?.id ?? nonTTYLastLogId;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const nextTaskSignatureById = new Map<number, string>();
|
|
238
|
-
const changedTaskLines: Array<string> = [];
|
|
239
|
-
const nonTtyUpdateStep = Math.max(1, Math.floor(config.nonTtyUpdateStep));
|
|
240
|
-
|
|
241
|
-
for (let i = 0; i < ordered.length; i++) {
|
|
242
|
-
const taskId = ordered[i]!.snapshot.id as number;
|
|
243
|
-
const snapshot = ordered[i]!.snapshot;
|
|
244
|
-
const line = taskLines[i]!;
|
|
245
|
-
const signature =
|
|
246
|
-
snapshot.units._tag === "DeterminateTaskUnits"
|
|
247
|
-
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
248
|
-
: `${snapshot.status}:${snapshot.description}`;
|
|
249
|
-
|
|
250
|
-
nextTaskSignatureById.set(taskId, signature);
|
|
251
|
-
if (nonTTYTaskSignatureById.get(taskId) !== signature) {
|
|
252
|
-
changedTaskLines.push(line);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (changedTaskLines.length > 0) {
|
|
257
|
-
process.stderr.write(changedTaskLines.join("\n") + "\n");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
nonTTYTaskSignatureById = nextTaskSignatureById;
|
|
250
|
+
if (!rendererActive) {
|
|
251
|
+
return;
|
|
261
252
|
}
|
|
262
253
|
|
|
254
|
+
yield* renderFrame("final");
|
|
255
|
+
|
|
263
256
|
if (isTTY) {
|
|
264
257
|
teardownInput?.();
|
|
265
258
|
process.stderr.write("\n" + SHOW_CURSOR);
|
package/src/runtime.ts
CHANGED
|
@@ -1,43 +1,41 @@
|
|
|
1
|
-
import { Effect, Exit, FiberRef, Option, Ref } from "effect";
|
|
1
|
+
import { Console, Effect, Exit, FiberRef, Option, Ref } from "effect";
|
|
2
|
+
import { mergeWith } from "es-toolkit/object";
|
|
2
3
|
import { formatWithOptions } from "node:util";
|
|
4
|
+
import type { PartialDeep } from "type-fest";
|
|
5
|
+
import { makeProgressConsole } from "./console";
|
|
3
6
|
import { runProgressServiceRenderer } from "./renderer";
|
|
4
|
-
import type {
|
|
5
|
-
AddTaskOptions,
|
|
6
|
-
ProgressService,
|
|
7
|
-
TrackOptions,
|
|
8
|
-
UpdateTaskOptions,
|
|
9
|
-
} from "./types";
|
|
7
|
+
import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
|
|
10
8
|
import {
|
|
9
|
+
decodeProgressBarConfigSync,
|
|
10
|
+
decodeRendererConfigSync,
|
|
11
11
|
defaultProgressBarConfig,
|
|
12
|
+
defaultRendererConfig,
|
|
12
13
|
DeterminateTaskUnits,
|
|
13
14
|
IndeterminateTaskUnits,
|
|
15
|
+
Progress,
|
|
14
16
|
ProgressBarConfig,
|
|
17
|
+
RendererConfig,
|
|
15
18
|
TaskId,
|
|
16
19
|
TaskSnapshot,
|
|
17
20
|
} from "./types";
|
|
21
|
+
import { inferTotal } from "./utils";
|
|
18
22
|
|
|
19
23
|
const DIRTY_DEBOUNCE_INTERVAL = "10 millis";
|
|
20
24
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (typeof candidate.size === "number") {
|
|
36
|
-
return candidate.size;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return undefined;
|
|
40
|
-
};
|
|
25
|
+
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
26
|
+
base: T,
|
|
27
|
+
override: PartialDeep<T> | undefined,
|
|
28
|
+
): T =>
|
|
29
|
+
mergeWith(
|
|
30
|
+
structuredClone(base),
|
|
31
|
+
(override ?? {}) as Record<PropertyKey, any>,
|
|
32
|
+
(_targetValue, sourceValue) => {
|
|
33
|
+
if (Array.isArray(sourceValue)) {
|
|
34
|
+
return sourceValue;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
) as T;
|
|
41
39
|
|
|
42
40
|
const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
|
|
43
41
|
const currentUnits = snapshot.units;
|
|
@@ -78,22 +76,51 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
78
76
|
status: snapshot.status,
|
|
79
77
|
transient: options.transient ?? snapshot.transient,
|
|
80
78
|
units,
|
|
79
|
+
progressbar: snapshot.progressbar,
|
|
81
80
|
});
|
|
82
81
|
};
|
|
83
82
|
|
|
84
83
|
export const makeProgressService = Effect.gen(function* () {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
84
|
+
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
85
|
+
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
86
|
+
|
|
87
|
+
const rendererConfig = decodeRendererConfigSync(
|
|
88
|
+
mergeConfig(
|
|
89
|
+
defaultRendererConfig,
|
|
90
|
+
Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
const progressBarConfig = decodeProgressBarConfigSync(
|
|
94
|
+
mergeConfig(
|
|
95
|
+
defaultProgressBarConfig,
|
|
96
|
+
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
87
100
|
|
|
88
101
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
89
102
|
const tasksRef = yield* Ref.make(new Map<TaskId, TaskSnapshot>());
|
|
90
|
-
const logsRef = yield* Ref.make<ReadonlyArray<
|
|
103
|
+
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
104
|
+
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
91
105
|
const dirtyRef = yield* Ref.make(true);
|
|
92
106
|
const dirtyScheduledRef = yield* Ref.make(false);
|
|
93
|
-
const
|
|
107
|
+
const rendererStartedRef = yield* Ref.make(false);
|
|
108
|
+
const rendererLatch = yield* Effect.makeLatch(false);
|
|
94
109
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
const scope = yield* Effect.scope;
|
|
111
|
+
|
|
112
|
+
yield* Effect.forkIn(
|
|
113
|
+
runProgressServiceRenderer(
|
|
114
|
+
tasksRef,
|
|
115
|
+
logsRef,
|
|
116
|
+
pendingLogsRef,
|
|
117
|
+
dirtyRef,
|
|
118
|
+
rendererConfig,
|
|
119
|
+
maxRetainedLogLines,
|
|
120
|
+
rendererLatch,
|
|
121
|
+
),
|
|
122
|
+
scope,
|
|
123
|
+
);
|
|
97
124
|
|
|
98
125
|
const markDirty = Effect.gen(function* () {
|
|
99
126
|
const shouldSchedule = yield* Ref.modify(dirtyScheduledRef, (scheduled) =>
|
|
@@ -123,6 +150,19 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
123
150
|
options.total === undefined || options.total <= 0
|
|
124
151
|
? new IndeterminateTaskUnits({ spinnerFrame: 0 })
|
|
125
152
|
: new DeterminateTaskUnits({ completed: 0, total: Math.max(0, options.total) });
|
|
153
|
+
const tasks = yield* Ref.get(tasksRef);
|
|
154
|
+
const parentSnapshot =
|
|
155
|
+
Option.isSome(parentId) ? tasks.get(parentId.value) : undefined;
|
|
156
|
+
const inheritedProgressBarConfig = parentSnapshot?.progressbar ?? progressBarConfig;
|
|
157
|
+
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
158
|
+
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
159
|
+
);
|
|
160
|
+
const shouldOpenRenderer = yield* Ref.modify(rendererStartedRef, (started) =>
|
|
161
|
+
started ? [false, true] : [true, true],
|
|
162
|
+
);
|
|
163
|
+
if (shouldOpenRenderer) {
|
|
164
|
+
yield* rendererLatch.open;
|
|
165
|
+
}
|
|
126
166
|
|
|
127
167
|
const snapshot = new TaskSnapshot({
|
|
128
168
|
id: taskId,
|
|
@@ -131,6 +171,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
131
171
|
status: "running",
|
|
132
172
|
transient: options.transient ?? false,
|
|
133
173
|
units,
|
|
174
|
+
progressbar: resolvedProgressBarConfig,
|
|
134
175
|
});
|
|
135
176
|
|
|
136
177
|
yield* Ref.update(tasksRef, (tasks) => {
|
|
@@ -170,8 +211,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
170
211
|
total: snapshot.units.total,
|
|
171
212
|
})
|
|
172
213
|
: new IndeterminateTaskUnits({
|
|
173
|
-
spinnerFrame:
|
|
174
|
-
(snapshot.units.spinnerFrame + amount) % Math.max(1, config.spinnerFrames.length),
|
|
214
|
+
spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount),
|
|
175
215
|
});
|
|
176
216
|
|
|
177
217
|
next.set(
|
|
@@ -183,6 +223,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
183
223
|
status: snapshot.status,
|
|
184
224
|
transient: snapshot.transient,
|
|
185
225
|
units,
|
|
226
|
+
progressbar: snapshot.progressbar,
|
|
186
227
|
}),
|
|
187
228
|
);
|
|
188
229
|
|
|
@@ -217,6 +258,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
217
258
|
total: snapshot.units.total,
|
|
218
259
|
})
|
|
219
260
|
: snapshot.units,
|
|
261
|
+
progressbar: snapshot.progressbar,
|
|
220
262
|
}),
|
|
221
263
|
);
|
|
222
264
|
return next;
|
|
@@ -244,6 +286,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
244
286
|
status: "failed",
|
|
245
287
|
transient: snapshot.transient,
|
|
246
288
|
units: snapshot.units,
|
|
289
|
+
progressbar: snapshot.progressbar,
|
|
247
290
|
}),
|
|
248
291
|
);
|
|
249
292
|
return next;
|
|
@@ -251,29 +294,45 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
251
294
|
|
|
252
295
|
const log = (...args: ReadonlyArray<unknown>) =>
|
|
253
296
|
Effect.gen(function* () {
|
|
254
|
-
|
|
297
|
+
if (args.length === 0) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const shouldOpenRenderer = yield* Ref.modify(rendererStartedRef, (started) =>
|
|
302
|
+
started ? [false, true] : [true, true],
|
|
303
|
+
);
|
|
304
|
+
if (shouldOpenRenderer) {
|
|
305
|
+
yield* rendererLatch.open;
|
|
306
|
+
}
|
|
307
|
+
|
|
255
308
|
const message = formatWithOptions(
|
|
256
309
|
{
|
|
257
|
-
colors:
|
|
310
|
+
colors: rendererConfig.isTTY,
|
|
258
311
|
depth: 6,
|
|
259
312
|
},
|
|
260
313
|
...args,
|
|
261
314
|
);
|
|
262
315
|
|
|
263
|
-
yield* Ref.update(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return next;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
316
|
+
yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
|
|
317
|
+
if (maxRetainedLogLines > 0) {
|
|
318
|
+
yield* Ref.update(logsRef, (logs) => {
|
|
319
|
+
const next = [...logs, message];
|
|
320
|
+
if (next.length <= maxRetainedLogLines) {
|
|
321
|
+
return next;
|
|
322
|
+
}
|
|
323
|
+
return next.slice(next.length - maxRetainedLogLines);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
274
327
|
yield* markDirty;
|
|
275
328
|
});
|
|
276
329
|
|
|
330
|
+
const withCapturedLogs: ProgressService["withCapturedLogs"] = (effect) =>
|
|
331
|
+
Effect.gen(function* () {
|
|
332
|
+
const outerConsole = yield* Console.consoleWith((console) => Effect.succeed(console));
|
|
333
|
+
return yield* Effect.withConsole(effect, makeProgressConsole(log, outerConsole));
|
|
334
|
+
});
|
|
335
|
+
|
|
277
336
|
const getTask = (taskId: TaskId) =>
|
|
278
337
|
Ref.get(tasksRef).pipe(Effect.map((tasks) => Option.fromNullable(tasks.get(taskId))));
|
|
279
338
|
|
|
@@ -313,10 +372,10 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
313
372
|
description: options.description,
|
|
314
373
|
total: options.total ?? inferTotal(iterable),
|
|
315
374
|
transient: options.transient,
|
|
375
|
+
progressbar: options.progressbar,
|
|
316
376
|
},
|
|
317
377
|
(taskId) => {
|
|
318
|
-
|
|
319
|
-
return Effect.forEach(items, (item, index) =>
|
|
378
|
+
return Effect.forEach(iterable, (item, index) =>
|
|
320
379
|
Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
|
|
321
380
|
);
|
|
322
381
|
},
|
|
@@ -329,11 +388,12 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
329
388
|
completeTask,
|
|
330
389
|
failTask,
|
|
331
390
|
log,
|
|
391
|
+
withCapturedLogs,
|
|
332
392
|
getTask,
|
|
333
393
|
listTasks,
|
|
334
394
|
withTask,
|
|
335
395
|
trackIterable,
|
|
336
396
|
};
|
|
337
397
|
|
|
338
|
-
return service;
|
|
398
|
+
return Progress.of(service);
|
|
339
399
|
});
|