effective-progress 0.1.0 → 0.1.2
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 +10 -4
- package/package.json +14 -9
- package/src/api.ts +42 -61
- package/src/renderer.ts +82 -121
- package/src/runtime.ts +34 -48
- package/src/types.ts +2 -11
- package/src/utils.ts +20 -0
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# effective-progress
|
|
2
2
|
|
|
3
|
-
`effective-progress` is an Effect-first terminal progress library with:
|
|
3
|
+
`effective-progress` is an [Effect](https://effect.website/)-first terminal progress library with:
|
|
4
4
|
|
|
5
5
|
- multiple progress bars
|
|
6
6
|
- nested child progress bars
|
|
7
7
|
- spinner support for indeterminate work
|
|
8
|
-
- clean log rendering alongside progress output
|
|
8
|
+
- clean log rendering alongside progress output, allowing you to simply use Effects `Console.log` or `Effect.logInfo`.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ const program = Progress.all(
|
|
|
26
26
|
yield* Console.log(`Completed task ${i + 1}`);
|
|
27
27
|
}),
|
|
28
28
|
),
|
|
29
|
-
{ description: "Running tasks in parallel",
|
|
29
|
+
{ description: "Running tasks in parallel", concurrency: 2 },
|
|
30
30
|
);
|
|
31
31
|
|
|
32
32
|
Effect.runPromise(program);
|
|
@@ -62,7 +62,7 @@ const program = Progress.all(
|
|
|
62
62
|
),
|
|
63
63
|
),
|
|
64
64
|
),
|
|
65
|
-
{ description: "Running tasks in parallel",
|
|
65
|
+
{ description: "Running tasks in parallel", concurrency: 2 },
|
|
66
66
|
);
|
|
67
67
|
|
|
68
68
|
Effect.runPromise(program);
|
|
@@ -80,6 +80,12 @@ Effect.runPromise(program);
|
|
|
80
80
|
- `examples/simpleExample.ts` - low-boilerplate real-world flow
|
|
81
81
|
- `examples/advancedExample.ts` - full API usage with custom config and manual task control
|
|
82
82
|
|
|
83
|
+
## Log retention
|
|
84
|
+
|
|
85
|
+
- `maxLogLines` controls in-memory log retention.
|
|
86
|
+
- `maxLogLines` omitted or set to `0` means no log history is kept in memory.
|
|
87
|
+
- `maxLogLines > 0` keeps only the latest `N` log lines in memory.
|
|
88
|
+
|
|
83
89
|
## Notes
|
|
84
90
|
|
|
85
91
|
- This is a WIP library, so expect breaking changes. Feedback and contributions are very welcome!
|
package/package.json
CHANGED
|
@@ -1,37 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effective-progress",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Effect-first terminal progress bars with nested multibar support",
|
|
5
|
+
"homepage": "https://github.com/stromseng/effective-progress#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/stromseng/effective-progress/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "git+https://github.com/stromseng/effective-progress.git"
|
|
9
13
|
},
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/stromseng/effective-progress/issues"
|
|
12
|
-
},
|
|
13
|
-
"homepage": "https://github.com/stromseng/effective-progress#readme",
|
|
14
|
-
"type": "module",
|
|
15
|
-
"module": "index.ts",
|
|
16
14
|
"files": [
|
|
17
15
|
"index.ts",
|
|
18
16
|
"src",
|
|
19
17
|
"README.md",
|
|
20
18
|
"LICENSE"
|
|
21
19
|
],
|
|
20
|
+
"type": "module",
|
|
21
|
+
"module": "index.ts",
|
|
22
22
|
"publishConfig": {
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
-
"prepare": "effect-language-service patch"
|
|
26
|
+
"prepare": "effect-language-service patch",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "oxlint .",
|
|
29
|
+
"format": "oxfmt --write .",
|
|
30
|
+
"format:check": "oxfmt --check ."
|
|
27
31
|
},
|
|
28
32
|
"dependencies": {
|
|
29
|
-
"@effect/language-service": "^0.73.1",
|
|
30
33
|
"chalk": "^5.6.2",
|
|
31
34
|
"effect": "^3.19.16"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
37
|
+
"@effect/language-service": "^0.73.1",
|
|
34
38
|
"@types/bun": "latest",
|
|
39
|
+
"oxfmt": "^0.32.0",
|
|
35
40
|
"oxlint": "^1.47.0"
|
|
36
41
|
},
|
|
37
42
|
"peerDependencies": {
|
package/src/api.ts
CHANGED
|
@@ -1,48 +1,36 @@
|
|
|
1
1
|
import { Console, Effect, Option } from "effect";
|
|
2
|
+
import type { Concurrency } from "effect/Types";
|
|
2
3
|
import { makeProgressConsole } from "./console";
|
|
3
4
|
import { makeProgressService } from "./runtime";
|
|
4
5
|
import { Progress } from "./types";
|
|
5
6
|
import type { TrackOptions } from "./types";
|
|
7
|
+
import { inferTotal } from "./utils";
|
|
6
8
|
|
|
7
|
-
export interface
|
|
8
|
-
readonly
|
|
9
|
-
readonly
|
|
10
|
-
readonly
|
|
11
|
-
readonly all?: {
|
|
12
|
-
readonly concurrency?: number | "unbounded";
|
|
13
|
-
readonly batching?: boolean | "inherit";
|
|
14
|
-
readonly concurrentFinalizers?: boolean;
|
|
15
|
-
};
|
|
9
|
+
export interface EffectExecutionOptions {
|
|
10
|
+
readonly concurrency?: Concurrency;
|
|
11
|
+
readonly batching?: boolean | "inherit";
|
|
12
|
+
readonly concurrentFinalizers?: boolean;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
export interface
|
|
19
|
-
readonly
|
|
20
|
-
|
|
21
|
-
readonly batching?: boolean | "inherit";
|
|
22
|
-
readonly concurrentFinalizers?: boolean;
|
|
23
|
-
};
|
|
15
|
+
export interface EffectAllExecutionOptions extends EffectExecutionOptions {
|
|
16
|
+
readonly discard?: boolean;
|
|
17
|
+
readonly mode?: "default" | "validate" | "either";
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
20
|
+
export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
|
|
21
|
+
export type AllReturn<
|
|
22
|
+
Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
|
|
23
|
+
O extends EffectAllExecutionOptions,
|
|
24
|
+
> =
|
|
25
|
+
[Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends
|
|
26
|
+
[Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress>>
|
|
27
|
+
: never;
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const candidate = iterable as { length?: unknown; size?: unknown };
|
|
36
|
-
if (typeof candidate.length === "number") {
|
|
37
|
-
return candidate.length;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (typeof candidate.size === "number") {
|
|
41
|
-
return candidate.size;
|
|
42
|
-
}
|
|
29
|
+
export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
30
|
+
readonly discard?: false | undefined;
|
|
31
|
+
}
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
};
|
|
33
|
+
export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
46
34
|
|
|
47
35
|
export const withProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
48
36
|
Effect.gen(function* () {
|
|
@@ -65,32 +53,36 @@ export const withProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
|
65
53
|
);
|
|
66
54
|
});
|
|
67
55
|
|
|
68
|
-
export const all = <
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
56
|
+
export const all = <
|
|
57
|
+
const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
|
|
58
|
+
O extends EffectAllExecutionOptions,
|
|
59
|
+
>(
|
|
60
|
+
effects: Arg,
|
|
61
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
62
|
+
): AllReturn<Arg, O> =>
|
|
72
63
|
withProgressService(
|
|
73
64
|
Effect.gen(function* () {
|
|
74
65
|
const progress = yield* Progress;
|
|
75
66
|
return yield* progress.withTask(
|
|
76
67
|
{
|
|
77
68
|
description: options.description,
|
|
78
|
-
total:
|
|
69
|
+
total: effects.length,
|
|
79
70
|
transient: options.transient,
|
|
80
71
|
},
|
|
81
72
|
(taskId) =>
|
|
82
|
-
Effect.
|
|
73
|
+
Effect.all(
|
|
83
74
|
effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
|
|
84
|
-
(effect) => effect,
|
|
85
75
|
{
|
|
86
|
-
concurrency: options.
|
|
87
|
-
batching: options.
|
|
88
|
-
|
|
76
|
+
concurrency: options.concurrency,
|
|
77
|
+
batching: options.batching,
|
|
78
|
+
discard: options.discard,
|
|
79
|
+
mode: options.mode,
|
|
80
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
89
81
|
},
|
|
90
82
|
),
|
|
91
83
|
);
|
|
92
84
|
}),
|
|
93
|
-
)
|
|
85
|
+
) as AllReturn<Arg, O>;
|
|
94
86
|
|
|
95
87
|
export const forEach = <A, B, E, R>(
|
|
96
88
|
iterable: Iterable<A>,
|
|
@@ -101,34 +93,23 @@ export const forEach = <A, B, E, R>(
|
|
|
101
93
|
Effect.gen(function* () {
|
|
102
94
|
const progress = yield* Progress;
|
|
103
95
|
|
|
104
|
-
const items = Array.from(iterable);
|
|
105
|
-
const total = options.total ?? inferTotal(iterable);
|
|
106
|
-
|
|
107
96
|
return yield* progress.withTask(
|
|
108
97
|
{
|
|
109
98
|
description: options.description,
|
|
110
|
-
total,
|
|
99
|
+
total: options.total ?? inferTotal(iterable),
|
|
111
100
|
transient: options.transient,
|
|
112
101
|
},
|
|
113
102
|
(taskId) =>
|
|
114
103
|
Effect.forEach(
|
|
115
|
-
|
|
104
|
+
iterable,
|
|
116
105
|
(item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
|
|
117
106
|
{
|
|
118
|
-
concurrency: options.
|
|
119
|
-
batching: options.
|
|
120
|
-
|
|
107
|
+
concurrency: options.concurrency,
|
|
108
|
+
batching: options.batching,
|
|
109
|
+
discard: options.discard,
|
|
110
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
121
111
|
},
|
|
122
112
|
),
|
|
123
113
|
);
|
|
124
114
|
}),
|
|
125
115
|
);
|
|
126
|
-
|
|
127
|
-
export const trackProgress = all;
|
|
128
|
-
|
|
129
|
-
export const track = <A, B, E, R>(
|
|
130
|
-
iterable: Iterable<A>,
|
|
131
|
-
options: TrackOptions,
|
|
132
|
-
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
133
|
-
): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress>> =>
|
|
134
|
-
forEach(iterable, f, options);
|
package/src/renderer.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { Effect, Ref } from "effect";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import type { ProgressBarConfigShape } from "./types";
|
|
4
|
-
import {
|
|
5
|
-
DeterminateTaskUnits,
|
|
6
|
-
TaskId,
|
|
7
|
-
TaskSnapshot,
|
|
8
|
-
} from "./types";
|
|
4
|
+
import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
|
|
9
5
|
|
|
10
6
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
11
7
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
@@ -13,11 +9,6 @@ const CLEAR_LINE = "\x1b[2K";
|
|
|
13
9
|
const MOVE_UP_ONE = "\x1b[1A";
|
|
14
10
|
const RENDER_INTERVAL = "80 millis";
|
|
15
11
|
|
|
16
|
-
export interface LogEntry {
|
|
17
|
-
readonly id: number;
|
|
18
|
-
readonly message: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
12
|
const renderDeterminate = (units: DeterminateTaskUnits, config: ProgressBarConfigShape): string => {
|
|
22
13
|
const safeTotal = units.total <= 0 ? 1 : units.total;
|
|
23
14
|
const ratio = Math.min(1, Math.max(0, units.completed / safeTotal));
|
|
@@ -80,82 +71,105 @@ const orderTasksForRender = (
|
|
|
80
71
|
|
|
81
72
|
export const runProgressServiceRenderer = (
|
|
82
73
|
tasksRef: Ref.Ref<Map<TaskId, TaskSnapshot>>,
|
|
83
|
-
logsRef: Ref.Ref<ReadonlyArray<
|
|
74
|
+
logsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
75
|
+
pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
84
76
|
dirtyRef: Ref.Ref<boolean>,
|
|
85
77
|
config: ProgressBarConfigShape,
|
|
78
|
+
maxRetainedLogLines: number,
|
|
86
79
|
) => {
|
|
87
80
|
const isTTY = config.isTTY;
|
|
81
|
+
const retainLogHistory = maxRetainedLogLines > 0;
|
|
88
82
|
let previousLineCount = 0;
|
|
89
|
-
let
|
|
83
|
+
let previousTaskLineCount = 0;
|
|
90
84
|
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
91
85
|
let tick = 0;
|
|
92
86
|
let teardownInput: (() => void) | undefined;
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
output += MOVE_UP_ONE + CLEAR_LINE;
|
|
99
|
-
}
|
|
100
|
-
process.stderr.write(output + "\r");
|
|
101
|
-
previousLineCount = 0;
|
|
102
|
-
};
|
|
88
|
+
const clearTTYLines = (lineCount: number) => {
|
|
89
|
+
if (lineCount <= 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
103
92
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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];
|
|
93
|
+
let output = "\r" + CLEAR_LINE;
|
|
94
|
+
for (let i = 1; i < lineCount; i++) {
|
|
95
|
+
output += MOVE_UP_ONE + CLEAR_LINE;
|
|
96
|
+
}
|
|
97
|
+
process.stderr.write(output + "\r");
|
|
98
|
+
};
|
|
118
99
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
100
|
+
const renderNonTTYTaskUpdates = (
|
|
101
|
+
ordered: ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }>,
|
|
102
|
+
taskLines: ReadonlyArray<string>,
|
|
103
|
+
) => {
|
|
104
|
+
const nextTaskSignatureById = new Map<number, string>();
|
|
105
|
+
const changedTaskLines: Array<string> = [];
|
|
106
|
+
const nonTtyUpdateStep = Math.max(1, Math.floor(config.nonTtyUpdateStep));
|
|
107
|
+
|
|
108
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
109
|
+
const taskId = ordered[i]!.snapshot.id as number;
|
|
110
|
+
const snapshot = ordered[i]!.snapshot;
|
|
111
|
+
const line = taskLines[i]!;
|
|
112
|
+
const signature =
|
|
113
|
+
snapshot.units._tag === "DeterminateTaskUnits"
|
|
114
|
+
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
115
|
+
: `${snapshot.status}:${snapshot.description}`;
|
|
116
|
+
|
|
117
|
+
nextTaskSignatureById.set(taskId, signature);
|
|
118
|
+
if (nonTTYTaskSignatureById.get(taskId) !== signature) {
|
|
119
|
+
changedTaskLines.push(line);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
131
122
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
if (changedTaskLines.length > 0) {
|
|
124
|
+
process.stderr.write(changedTaskLines.join("\n") + "\n");
|
|
125
|
+
}
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const snapshot = ordered[i]!.snapshot;
|
|
139
|
-
const line = taskLines[i]!;
|
|
140
|
-
const signature =
|
|
141
|
-
snapshot.units._tag === "DeterminateTaskUnits"
|
|
142
|
-
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
143
|
-
: `${snapshot.status}:${snapshot.description}`;
|
|
127
|
+
nonTTYTaskSignatureById = nextTaskSignatureById;
|
|
128
|
+
};
|
|
144
129
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
130
|
+
const renderFrame = (mode: "tick" | "final") =>
|
|
131
|
+
Effect.gen(function* () {
|
|
132
|
+
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
133
|
+
const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
|
|
134
|
+
(task) => !(task.transient && task.status !== "running"),
|
|
135
|
+
);
|
|
136
|
+
const ordered = orderTasksForRender(snapshots);
|
|
137
|
+
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
138
|
+
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
139
|
+
const lineTick = isTTY ? frameTick : 0;
|
|
140
|
+
return buildTaskLine(snapshot, depth, lineTick, config);
|
|
141
|
+
});
|
|
150
142
|
|
|
151
|
-
|
|
152
|
-
|
|
143
|
+
if (isTTY) {
|
|
144
|
+
if (retainLogHistory) {
|
|
145
|
+
const historyLogs = yield* Ref.get(logsRef);
|
|
146
|
+
const lines = [...historyLogs, ...taskLines];
|
|
147
|
+
clearTTYLines(previousLineCount);
|
|
148
|
+
if (lines.length > 0) {
|
|
149
|
+
process.stderr.write(lines.join("\n"));
|
|
153
150
|
}
|
|
151
|
+
previousLineCount = lines.length;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
154
|
|
|
155
|
-
|
|
155
|
+
clearTTYLines(previousTaskLineCount);
|
|
156
|
+
if (drainedLogs.length > 0) {
|
|
157
|
+
process.stderr.write(drainedLogs.join("\n") + "\n");
|
|
156
158
|
}
|
|
157
|
-
|
|
159
|
+
if (taskLines.length > 0) {
|
|
160
|
+
process.stderr.write(taskLines.join("\n"));
|
|
161
|
+
}
|
|
162
|
+
previousTaskLineCount = taskLines.length;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
158
165
|
|
|
166
|
+
if (drainedLogs.length > 0) {
|
|
167
|
+
process.stderr.write(drainedLogs.join("\n") + "\n");
|
|
168
|
+
}
|
|
169
|
+
renderNonTTYTaskUpdates(ordered, taskLines);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return Effect.gen(function* () {
|
|
159
173
|
if (isTTY) {
|
|
160
174
|
process.stderr.write(HIDE_CURSOR);
|
|
161
175
|
|
|
@@ -205,60 +219,7 @@ export const runProgressServiceRenderer = (
|
|
|
205
219
|
}).pipe(
|
|
206
220
|
Effect.ensuring(
|
|
207
221
|
Effect.gen(function* () {
|
|
208
|
-
|
|
209
|
-
const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
|
|
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;
|
|
261
|
-
}
|
|
222
|
+
yield* renderFrame("final");
|
|
262
223
|
|
|
263
224
|
if (isTTY) {
|
|
264
225
|
teardownInput?.();
|
package/src/runtime.ts
CHANGED
|
@@ -1,44 +1,20 @@
|
|
|
1
1
|
import { Effect, Exit, FiberRef, Option, Ref } from "effect";
|
|
2
2
|
import { formatWithOptions } from "node:util";
|
|
3
3
|
import { runProgressServiceRenderer } from "./renderer";
|
|
4
|
-
import type {
|
|
5
|
-
AddTaskOptions,
|
|
6
|
-
ProgressService,
|
|
7
|
-
TrackOptions,
|
|
8
|
-
UpdateTaskOptions,
|
|
9
|
-
} from "./types";
|
|
4
|
+
import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
|
|
10
5
|
import {
|
|
11
6
|
defaultProgressBarConfig,
|
|
12
7
|
DeterminateTaskUnits,
|
|
13
8
|
IndeterminateTaskUnits,
|
|
9
|
+
Progress,
|
|
14
10
|
ProgressBarConfig,
|
|
15
11
|
TaskId,
|
|
16
12
|
TaskSnapshot,
|
|
17
13
|
} from "./types";
|
|
14
|
+
import { inferTotal } from "./utils";
|
|
18
15
|
|
|
19
16
|
const DIRTY_DEBOUNCE_INTERVAL = "10 millis";
|
|
20
17
|
|
|
21
|
-
const inferTotal = (iterable: Iterable<unknown>): number | undefined => {
|
|
22
|
-
if (Array.isArray(iterable)) {
|
|
23
|
-
return iterable.length;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (typeof iterable === "string") {
|
|
27
|
-
return iterable.length;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const candidate = iterable as { length?: unknown; size?: unknown };
|
|
31
|
-
if (typeof candidate.length === "number") {
|
|
32
|
-
return candidate.length;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (typeof candidate.size === "number") {
|
|
36
|
-
return candidate.size;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return undefined;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
18
|
const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
|
|
43
19
|
const currentUnits = snapshot.units;
|
|
44
20
|
const units = (() => {
|
|
@@ -84,16 +60,26 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
84
60
|
export const makeProgressService = Effect.gen(function* () {
|
|
85
61
|
const configOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
86
62
|
const config = Option.getOrElse(configOption, () => defaultProgressBarConfig);
|
|
63
|
+
const maxRetainedLogLines = Math.max(0, Math.floor(config.maxLogLines ?? 0));
|
|
87
64
|
|
|
88
65
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
89
66
|
const tasksRef = yield* Ref.make(new Map<TaskId, TaskSnapshot>());
|
|
90
|
-
const logsRef = yield* Ref.make<ReadonlyArray<
|
|
67
|
+
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
68
|
+
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
91
69
|
const dirtyRef = yield* Ref.make(true);
|
|
92
70
|
const dirtyScheduledRef = yield* Ref.make(false);
|
|
93
|
-
const nextLogIdRef = yield* Ref.make(0);
|
|
94
71
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
95
72
|
|
|
96
|
-
yield* Effect.forkScoped(
|
|
73
|
+
yield* Effect.forkScoped(
|
|
74
|
+
runProgressServiceRenderer(
|
|
75
|
+
tasksRef,
|
|
76
|
+
logsRef,
|
|
77
|
+
pendingLogsRef,
|
|
78
|
+
dirtyRef,
|
|
79
|
+
config,
|
|
80
|
+
maxRetainedLogLines,
|
|
81
|
+
),
|
|
82
|
+
);
|
|
97
83
|
|
|
98
84
|
const markDirty = Effect.gen(function* () {
|
|
99
85
|
const shouldSchedule = yield* Ref.modify(dirtyScheduledRef, (scheduled) =>
|
|
@@ -251,7 +237,10 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
251
237
|
|
|
252
238
|
const log = (...args: ReadonlyArray<unknown>) =>
|
|
253
239
|
Effect.gen(function* () {
|
|
254
|
-
|
|
240
|
+
if (args.length === 0) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
255
244
|
const message = formatWithOptions(
|
|
256
245
|
{
|
|
257
246
|
colors: config.isTTY,
|
|
@@ -260,17 +249,17 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
260
249
|
...args,
|
|
261
250
|
);
|
|
262
251
|
|
|
263
|
-
yield* Ref.update(
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return next;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
|
|
252
|
+
yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
|
|
253
|
+
if (maxRetainedLogLines > 0) {
|
|
254
|
+
yield* Ref.update(logsRef, (logs) => {
|
|
255
|
+
const next = [...logs, message];
|
|
256
|
+
if (next.length <= maxRetainedLogLines) {
|
|
257
|
+
return next;
|
|
258
|
+
}
|
|
259
|
+
return next.slice(next.length - maxRetainedLogLines);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
274
263
|
yield* markDirty;
|
|
275
264
|
});
|
|
276
265
|
|
|
@@ -315,14 +304,13 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
315
304
|
transient: options.transient,
|
|
316
305
|
},
|
|
317
306
|
(taskId) => {
|
|
318
|
-
|
|
319
|
-
return Effect.forEach(items, (item, index) =>
|
|
307
|
+
return Effect.forEach(iterable, (item, index) =>
|
|
320
308
|
Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
|
|
321
309
|
);
|
|
322
310
|
},
|
|
323
311
|
);
|
|
324
312
|
|
|
325
|
-
|
|
313
|
+
return Progress.of({
|
|
326
314
|
addTask,
|
|
327
315
|
updateTask,
|
|
328
316
|
advanceTask,
|
|
@@ -333,7 +321,5 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
333
321
|
listTasks,
|
|
334
322
|
withTask,
|
|
335
323
|
trackIterable,
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
return service;
|
|
324
|
+
});
|
|
339
325
|
});
|
package/src/types.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const ProgressBarConfigSchema = Schema.Struct({
|
|
|
12
12
|
emptyChar: Schema.String,
|
|
13
13
|
leftBracket: Schema.String,
|
|
14
14
|
rightBracket: Schema.String,
|
|
15
|
-
maxLogLines: Schema.Number,
|
|
15
|
+
maxLogLines: Schema.optional(Schema.Number),
|
|
16
16
|
nonTtyUpdateStep: Schema.Number,
|
|
17
17
|
});
|
|
18
18
|
|
|
@@ -59,11 +59,7 @@ export interface UpdateTaskOptions {
|
|
|
59
59
|
readonly transient?: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
export
|
|
63
|
-
readonly description: string;
|
|
64
|
-
readonly total?: number;
|
|
65
|
-
readonly transient?: boolean;
|
|
66
|
-
}
|
|
62
|
+
export type TrackOptions = Exclude<AddTaskOptions, "parentId">;
|
|
67
63
|
|
|
68
64
|
export class DeterminateTaskUnits extends Schema.TaggedClass<DeterminateTaskUnits>()(
|
|
69
65
|
"DeterminateTaskUnits",
|
|
@@ -160,8 +156,3 @@ export const ProgressTaskEventSchema = Schema.Union(
|
|
|
160
156
|
export type ProgressTaskEvent = typeof ProgressTaskEventSchema.Type;
|
|
161
157
|
|
|
162
158
|
export const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
|
|
163
|
-
|
|
164
|
-
export const isIndeterminateTask = (snapshot: TaskSnapshot) =>
|
|
165
|
-
snapshot.units._tag === "IndeterminateTaskUnits";
|
|
166
|
-
|
|
167
|
-
export const nextSpinnerFrame = (index: number) => (index + 1) % SPINNER_FRAMES.length;
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const inferTotal = (iterable: Iterable<unknown>): number | undefined => {
|
|
2
|
+
if (Array.isArray(iterable)) {
|
|
3
|
+
return iterable.length;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof iterable === "string") {
|
|
7
|
+
return iterable.length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const candidate = iterable as { length?: unknown; size?: unknown };
|
|
11
|
+
if (typeof candidate.length === "number") {
|
|
12
|
+
return candidate.length;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof candidate.size === "number") {
|
|
16
|
+
return candidate.size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return undefined;
|
|
20
|
+
};
|