effective-progress 0.2.0 → 0.2.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 +23 -29
- package/package.json +1 -1
- package/src/api.ts +143 -79
- package/src/console.ts +2 -2
- package/src/renderer.ts +12 -33
- package/src/runtime.ts +134 -104
- package/src/terminal.ts +1 -3
- package/src/types.ts +29 -10
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/effective-progress)
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
|
-
> Pre-`1.0.0`, breaking changes may happen in any release. SemVer guarantees will begin at `1.0.0`.
|
|
6
|
+
> Pre-`1.0.0`, breaking changes may happen in any release. SemVer guarantees will begin at `1.0.0`.
|
|
7
7
|
> I recommend using only the `Progress.all` and `Progress.forEach` APIs for now, as they will likely change the least. The lower-level APIs for manual progress bar control are more likely to see breaking changes as I iterate on the design.
|
|
8
8
|
>
|
|
9
9
|
> Please open an issue or reach out if you have any questions or want to contribute!
|
|
@@ -115,6 +115,22 @@ const configured = program.pipe(
|
|
|
115
115
|
Effect.runPromise(configured);
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
Task-level `progressbar` config is optional and inherits from its parent task (or from global `ProgressBarConfig` for root tasks):
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
yield *
|
|
122
|
+
progress.withTask(Effect.sleep("1 second"), {
|
|
123
|
+
description: "Worker pipeline",
|
|
124
|
+
progressbar: {
|
|
125
|
+
barWidth: 20,
|
|
126
|
+
spinnerFrames: [".", "o", "O", "0"],
|
|
127
|
+
colors: {
|
|
128
|
+
spinner: { kind: "named", value: "magentaBright" },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
118
134
|
## Terminal service and mocking
|
|
119
135
|
|
|
120
136
|
`effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
|
|
@@ -125,7 +141,7 @@ Effect.runPromise(configured);
|
|
|
125
141
|
- `writeStderr(text)`
|
|
126
142
|
- `withRawInputCapture(effect)`
|
|
127
143
|
|
|
128
|
-
You can provide a mock
|
|
144
|
+
You can provide a mock if you want to alter the behavior of terminal detection or if you want to capture the output for testing:
|
|
129
145
|
|
|
130
146
|
```ts
|
|
131
147
|
import { Effect } from "effect";
|
|
@@ -139,45 +155,23 @@ const mockTerminal: Progress.ProgressTerminalService = {
|
|
|
139
155
|
withRawInputCapture: (effect) => effect,
|
|
140
156
|
};
|
|
141
157
|
|
|
142
|
-
const program = Progress.
|
|
158
|
+
const program = Progress.task(Effect.sleep("100 millis"), { description: "work" }).pipe(
|
|
143
159
|
Effect.provideService(Progress.ProgressTerminal, mockTerminal),
|
|
144
160
|
);
|
|
145
161
|
```
|
|
146
162
|
|
|
147
|
-
##
|
|
163
|
+
## Manual task control
|
|
148
164
|
|
|
149
|
-
`
|
|
150
|
-
TTY mode is now sourced from `ProgressTerminal.isTTY`.
|
|
151
|
-
|
|
152
|
-
Task-level `progressbar` config is optional and inherits from its parent task (or from global `ProgressBarConfig` for root tasks):
|
|
165
|
+
For manual usage, `task` captures logs implicitly and provides the current `Task` context:
|
|
153
166
|
|
|
154
167
|
```ts
|
|
155
|
-
|
|
156
|
-
progress.withTask(
|
|
157
|
-
{
|
|
158
|
-
description: "Worker pipeline",
|
|
159
|
-
progressbar: {
|
|
160
|
-
barWidth: 20,
|
|
161
|
-
spinnerFrames: [".", "o", "O", "0"],
|
|
162
|
-
colors: {
|
|
163
|
-
spinner: { kind: "named", value: "magentaBright" },
|
|
164
|
-
},
|
|
165
|
-
},
|
|
166
|
-
},
|
|
167
|
-
Effect.sleep("1 second"),
|
|
168
|
-
);
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
For manual usage, `withTask` captures logs implicitly and provides the current `Task` context:
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
const program = Progress.withTask(
|
|
175
|
-
{ description: "Manual task" },
|
|
168
|
+
const program = Progress.task(
|
|
176
169
|
Effect.gen(function* () {
|
|
177
170
|
const currentTask = yield* Progress.Task;
|
|
178
171
|
yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
|
|
179
172
|
yield* Effect.sleep("1 second");
|
|
180
173
|
}),
|
|
174
|
+
{ description: "Manual task" },
|
|
181
175
|
);
|
|
182
176
|
```
|
|
183
177
|
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
1
|
+
import { Effect, Exit } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
2
3
|
import type { Concurrency } from "effect/Types";
|
|
3
4
|
import { Progress, provideProgressService } from "./runtime";
|
|
4
5
|
import { Task } from "./types";
|
|
@@ -20,9 +21,10 @@ export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions
|
|
|
20
21
|
export type AllReturn<
|
|
21
22
|
Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
|
|
22
23
|
O extends EffectAllExecutionOptions,
|
|
23
|
-
> =
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
> = [Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends [
|
|
25
|
+
Effect.Effect<infer A, infer E, infer R>,
|
|
26
|
+
]
|
|
27
|
+
? Effect.Effect<A, E, Exclude<R, Progress | Task>>
|
|
26
28
|
: never;
|
|
27
29
|
|
|
28
30
|
export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
@@ -31,81 +33,143 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
|
31
33
|
|
|
32
34
|
export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
33
35
|
|
|
34
|
-
export const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
export const task: {
|
|
37
|
+
<A, E, R>(
|
|
38
|
+
effect: Effect.Effect<A, E, R>,
|
|
39
|
+
options: AddTaskOptions,
|
|
40
|
+
): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
41
|
+
<A, E, R>(
|
|
42
|
+
options: AddTaskOptions,
|
|
43
|
+
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
44
|
+
} = dual(
|
|
45
|
+
2,
|
|
46
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
47
|
+
provideProgressService(
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const progress = yield* Progress;
|
|
50
|
+
return yield* progress.withTask(effect, options);
|
|
51
|
+
}),
|
|
52
|
+
) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
|
|
53
|
+
);
|
|
44
54
|
|
|
45
|
-
export const all
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
55
|
+
export const all: {
|
|
56
|
+
<
|
|
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>;
|
|
63
|
+
<O extends EffectAllExecutionOptions>(
|
|
64
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
65
|
+
): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(
|
|
66
|
+
effects: Arg,
|
|
67
|
+
) => AllReturn<Arg, O>;
|
|
68
|
+
} = dual(
|
|
69
|
+
2,
|
|
70
|
+
<
|
|
71
|
+
const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
|
|
72
|
+
O extends EffectAllExecutionOptions,
|
|
73
|
+
>(
|
|
74
|
+
effects: Arg,
|
|
75
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
76
|
+
) =>
|
|
77
|
+
provideProgressService(
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
const progress = yield* Progress;
|
|
80
|
+
return yield* progress.runTask(
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const taskId = yield* Task;
|
|
83
|
+
const exit = yield* Effect.exit(
|
|
84
|
+
Effect.all(
|
|
85
|
+
effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
|
|
86
|
+
{
|
|
87
|
+
concurrency: options.concurrency,
|
|
88
|
+
batching: options.batching,
|
|
89
|
+
discard: options.discard,
|
|
90
|
+
mode: options.mode,
|
|
91
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
92
|
+
},
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (Exit.isSuccess(exit)) {
|
|
97
|
+
yield* progress.completeTask(taskId);
|
|
98
|
+
} else {
|
|
99
|
+
yield* progress.failTask(taskId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return yield* Exit.match(exit, {
|
|
103
|
+
onFailure: Effect.failCause,
|
|
104
|
+
onSuccess: Effect.succeed,
|
|
105
|
+
});
|
|
106
|
+
}),
|
|
107
|
+
{
|
|
108
|
+
description: options.description,
|
|
109
|
+
total: effects.length,
|
|
110
|
+
transient: options.transient,
|
|
111
|
+
progressbar: options.progressbar,
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
}),
|
|
115
|
+
) as AllReturn<Arg, O>,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const forEach: {
|
|
119
|
+
<A, B, E, R>(
|
|
120
|
+
iterable: Iterable<A>,
|
|
121
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
122
|
+
options: ForEachOptions,
|
|
123
|
+
): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
124
|
+
<A, B, E, R>(
|
|
125
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
126
|
+
options: ForEachOptions,
|
|
127
|
+
): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
128
|
+
} = dual(
|
|
129
|
+
3,
|
|
130
|
+
<A, B, E, R>(
|
|
131
|
+
iterable: Iterable<A>,
|
|
132
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
133
|
+
options: ForEachOptions,
|
|
134
|
+
) =>
|
|
135
|
+
provideProgressService(
|
|
136
|
+
Effect.gen(function* () {
|
|
137
|
+
const progress = yield* Progress;
|
|
138
|
+
|
|
139
|
+
return yield* progress.runTask(
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const taskId = yield* Task;
|
|
142
|
+
const exit = yield* Effect.exit(
|
|
143
|
+
Effect.forEach(
|
|
144
|
+
iterable,
|
|
145
|
+
(item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
|
|
146
|
+
{
|
|
147
|
+
concurrency: options.concurrency,
|
|
148
|
+
batching: options.batching,
|
|
149
|
+
discard: options.discard,
|
|
150
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
151
|
+
},
|
|
152
|
+
),
|
|
153
|
+
);
|
|
78
154
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
provideProgressService(
|
|
85
|
-
Effect.gen(function* () {
|
|
86
|
-
const progress = yield* Progress;
|
|
155
|
+
if (Exit.isSuccess(exit)) {
|
|
156
|
+
yield* progress.completeTask(taskId);
|
|
157
|
+
} else {
|
|
158
|
+
yield* progress.failTask(taskId);
|
|
159
|
+
}
|
|
87
160
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
batching: options.batching,
|
|
104
|
-
discard: options.discard,
|
|
105
|
-
concurrentFinalizers: options.concurrentFinalizers,
|
|
106
|
-
},
|
|
107
|
-
);
|
|
108
|
-
}),
|
|
109
|
-
);
|
|
110
|
-
}),
|
|
111
|
-
) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
161
|
+
return yield* Exit.match(exit, {
|
|
162
|
+
onFailure: Effect.failCause,
|
|
163
|
+
onSuccess: Effect.succeed,
|
|
164
|
+
});
|
|
165
|
+
}),
|
|
166
|
+
{
|
|
167
|
+
description: options.description,
|
|
168
|
+
total: options.total ?? inferTotal(iterable),
|
|
169
|
+
transient: options.transient,
|
|
170
|
+
progressbar: options.progressbar,
|
|
171
|
+
},
|
|
172
|
+
);
|
|
173
|
+
}),
|
|
174
|
+
) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>,
|
|
175
|
+
);
|
package/src/console.ts
CHANGED
|
@@ -12,7 +12,7 @@ export const makeProgressConsole = (
|
|
|
12
12
|
|
|
13
13
|
const delegate = (effect: Effect.Effect<void, never, never>) => effect;
|
|
14
14
|
|
|
15
|
-
return {
|
|
15
|
+
return Console.Console.of({
|
|
16
16
|
[Console.TypeId]: Console.TypeId,
|
|
17
17
|
assert(condition, ...args) {
|
|
18
18
|
return condition ? Effect.void : log("Assertion failed:", ...args);
|
|
@@ -81,5 +81,5 @@ export const makeProgressConsole = (
|
|
|
81
81
|
unsafeLog(...args);
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
|
-
};
|
|
84
|
+
});
|
|
85
85
|
};
|
package/src/renderer.ts
CHANGED
|
@@ -5,8 +5,8 @@ import {
|
|
|
5
5
|
ProgressBarColorsSchema,
|
|
6
6
|
} from "./colors";
|
|
7
7
|
import type { ProgressTerminalService } from "./terminal";
|
|
8
|
-
import type { ProgressBarConfigShape, RendererConfigShape } from "./types";
|
|
9
|
-
import { DeterminateTaskUnits,
|
|
8
|
+
import type { ProgressBarConfigShape, RendererConfigShape, TaskStore } from "./types";
|
|
9
|
+
import { DeterminateTaskUnits, TaskSnapshot } from "./types";
|
|
10
10
|
|
|
11
11
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
12
12
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
@@ -33,7 +33,7 @@ const buildTaskLine = (
|
|
|
33
33
|
tick: number,
|
|
34
34
|
colors: CompiledProgressBarColors,
|
|
35
35
|
): string => {
|
|
36
|
-
const progressbar = snapshot.
|
|
36
|
+
const progressbar = snapshot.config;
|
|
37
37
|
const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
|
|
38
38
|
|
|
39
39
|
if (snapshot.status === "failed") {
|
|
@@ -57,31 +57,8 @@ const buildTaskLine = (
|
|
|
57
57
|
return `${prefix}${colors.spinner(frame)}`;
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const orderTasksForRender = (
|
|
61
|
-
tasks: ReadonlyArray<TaskSnapshot>,
|
|
62
|
-
): ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }> => {
|
|
63
|
-
const byParent = new Map<number | null, Array<TaskSnapshot>>();
|
|
64
|
-
for (const task of tasks) {
|
|
65
|
-
const bucket = byParent.get(task.parentId) ?? [];
|
|
66
|
-
bucket.push(task);
|
|
67
|
-
byParent.set(task.parentId, bucket);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const ordered: Array<{ snapshot: TaskSnapshot; depth: number }> = [];
|
|
71
|
-
const visit = (parentId: number | null, depth: number) => {
|
|
72
|
-
const children = byParent.get(parentId) ?? [];
|
|
73
|
-
for (const child of children) {
|
|
74
|
-
ordered.push({ snapshot: child, depth });
|
|
75
|
-
visit(child.id, depth + 1);
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
visit(null, 0);
|
|
80
|
-
return ordered;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
60
|
export const runProgressServiceRenderer = (
|
|
84
|
-
|
|
61
|
+
storeRef: Ref.Ref<TaskStore>,
|
|
85
62
|
logsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
86
63
|
pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
87
64
|
dirtyRef: Ref.Ref<boolean>,
|
|
@@ -187,14 +164,16 @@ export const runProgressServiceRenderer = (
|
|
|
187
164
|
const renderFrame = (mode: "tick" | "final") =>
|
|
188
165
|
Effect.gen(function* () {
|
|
189
166
|
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
167
|
+
const store = yield* Ref.get(storeRef);
|
|
168
|
+
const ordered = store.renderOrder.flatMap((row) => {
|
|
169
|
+
const snapshot = store.tasks.get(row.id);
|
|
170
|
+
if (!snapshot || (snapshot.transient && snapshot.status !== "running")) return [];
|
|
171
|
+
return [{ snapshot, depth: row.depth }];
|
|
172
|
+
});
|
|
194
173
|
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
195
174
|
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
196
175
|
const lineTick = isTTY ? frameTick : 0;
|
|
197
|
-
return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.
|
|
176
|
+
return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.config));
|
|
198
177
|
});
|
|
199
178
|
|
|
200
179
|
if (isTTY) {
|
|
@@ -248,7 +227,7 @@ export const runProgressServiceRenderer = (
|
|
|
248
227
|
|
|
249
228
|
while (true) {
|
|
250
229
|
const dirty = yield* Ref.getAndSet(dirtyRef, false);
|
|
251
|
-
const tasks = Array.from((yield* Ref.get(
|
|
230
|
+
const tasks = Array.from((yield* Ref.get(storeRef)).tasks.values()).filter(
|
|
252
231
|
(task) => !(task.transient && task.status !== "running"),
|
|
253
232
|
);
|
|
254
233
|
const hasActiveSpinners = tasks.some(
|
package/src/runtime.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
2
3
|
import { mergeWith } from "es-toolkit/object";
|
|
3
4
|
import { formatWithOptions } from "node:util";
|
|
4
5
|
import type { PartialDeep } from "type-fest";
|
|
5
6
|
import { makeProgressConsole } from "./console";
|
|
6
7
|
import { runProgressServiceRenderer } from "./renderer";
|
|
7
8
|
import { ProgressTerminal } from "./terminal";
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AddTaskOptions,
|
|
11
|
+
ProgressService,
|
|
12
|
+
RenderRow,
|
|
13
|
+
TaskStore,
|
|
14
|
+
UpdateTaskOptions,
|
|
15
|
+
} from "./types";
|
|
9
16
|
import {
|
|
10
17
|
decodeProgressBarConfigSync,
|
|
11
18
|
decodeRendererConfigSync,
|
|
@@ -19,7 +26,6 @@ import {
|
|
|
19
26
|
TaskId,
|
|
20
27
|
TaskSnapshot,
|
|
21
28
|
} from "./types";
|
|
22
|
-
import { inferTotal } from "./utils";
|
|
23
29
|
|
|
24
30
|
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
25
31
|
base: T,
|
|
@@ -75,10 +81,39 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
75
81
|
status: snapshot.status,
|
|
76
82
|
transient: options.transient ?? snapshot.transient,
|
|
77
83
|
units,
|
|
78
|
-
|
|
84
|
+
config: snapshot.config,
|
|
79
85
|
});
|
|
80
86
|
};
|
|
81
87
|
|
|
88
|
+
const findInsertionIndex = (
|
|
89
|
+
renderOrder: ReadonlyArray<RenderRow>,
|
|
90
|
+
parentId: TaskId | null,
|
|
91
|
+
): { index: number; depth: number } => {
|
|
92
|
+
if (parentId === null) {
|
|
93
|
+
return { index: renderOrder.length, depth: 0 };
|
|
94
|
+
}
|
|
95
|
+
const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
|
|
96
|
+
if (parentIdx === -1) return { index: renderOrder.length, depth: 0 };
|
|
97
|
+
const parentDepth = renderOrder[parentIdx]!.depth;
|
|
98
|
+
let i = parentIdx + 1;
|
|
99
|
+
while (i < renderOrder.length && renderOrder[i]!.depth > parentDepth) i++;
|
|
100
|
+
return { index: i, depth: parentDepth + 1 };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const removeFromRenderOrder = (
|
|
104
|
+
renderOrder: ReadonlyArray<RenderRow>,
|
|
105
|
+
taskId: TaskId,
|
|
106
|
+
): ReadonlyArray<RenderRow> => {
|
|
107
|
+
const idx = renderOrder.findIndex((row) => row.id === taskId);
|
|
108
|
+
if (idx === -1) return renderOrder;
|
|
109
|
+
const taskDepth = renderOrder[idx]!.depth;
|
|
110
|
+
let end = idx + 1;
|
|
111
|
+
while (end < renderOrder.length && renderOrder[end]!.depth > taskDepth) end++;
|
|
112
|
+
const next = [...renderOrder];
|
|
113
|
+
next.splice(idx, end - idx);
|
|
114
|
+
return next;
|
|
115
|
+
};
|
|
116
|
+
|
|
82
117
|
const makeProgressService = Effect.gen(function* () {
|
|
83
118
|
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
84
119
|
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
@@ -100,7 +135,10 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
100
135
|
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
101
136
|
|
|
102
137
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
103
|
-
const
|
|
138
|
+
const storeRef = yield* Ref.make<TaskStore>({
|
|
139
|
+
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
140
|
+
renderOrder: [],
|
|
141
|
+
});
|
|
104
142
|
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
105
143
|
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
106
144
|
const dirtyRef = yield* Ref.make(true);
|
|
@@ -109,7 +147,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
109
147
|
|
|
110
148
|
yield* Effect.forkIn(
|
|
111
149
|
runProgressServiceRenderer(
|
|
112
|
-
|
|
150
|
+
storeRef,
|
|
113
151
|
logsRef,
|
|
114
152
|
pendingLogsRef,
|
|
115
153
|
dirtyRef,
|
|
@@ -125,7 +163,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
125
163
|
|
|
126
164
|
const addTask = (options: AddTaskOptions) =>
|
|
127
165
|
Effect.gen(function* () {
|
|
128
|
-
const
|
|
166
|
+
const resolvedParentId =
|
|
129
167
|
options.parentId === undefined
|
|
130
168
|
? yield* FiberRef.get(currentParentRef)
|
|
131
169
|
: Option.some(options.parentId);
|
|
@@ -134,27 +172,33 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
134
172
|
options.total === undefined || options.total <= 0
|
|
135
173
|
? new IndeterminateTaskUnits({ spinnerFrame: 0 })
|
|
136
174
|
: new DeterminateTaskUnits({ completed: 0, total: Math.max(0, options.total) });
|
|
137
|
-
const
|
|
138
|
-
const parentSnapshot = Option.isSome(
|
|
139
|
-
|
|
175
|
+
const store = yield* Ref.get(storeRef);
|
|
176
|
+
const parentSnapshot = Option.isSome(resolvedParentId)
|
|
177
|
+
? store.tasks.get(resolvedParentId.value)
|
|
178
|
+
: undefined;
|
|
179
|
+
const inheritedProgressBarConfig = parentSnapshot?.config ?? progressBarConfig;
|
|
140
180
|
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
141
181
|
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
142
182
|
);
|
|
143
183
|
|
|
184
|
+
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
144
185
|
const snapshot = new TaskSnapshot({
|
|
145
186
|
id: taskId,
|
|
146
|
-
parentId:
|
|
187
|
+
parentId: parentIdValue,
|
|
147
188
|
description: options.description,
|
|
148
189
|
status: "running",
|
|
149
190
|
transient: options.transient ?? false,
|
|
150
191
|
units,
|
|
151
|
-
|
|
192
|
+
config: resolvedProgressBarConfig,
|
|
152
193
|
});
|
|
153
194
|
|
|
154
|
-
yield* Ref.update(
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
195
|
+
yield* Ref.update(storeRef, (s) => {
|
|
196
|
+
const nextTasks = new Map(s.tasks);
|
|
197
|
+
nextTasks.set(taskId, snapshot);
|
|
198
|
+
const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
|
|
199
|
+
const nextOrder = [...s.renderOrder];
|
|
200
|
+
nextOrder.splice(index, 0, { id: taskId, depth });
|
|
201
|
+
return { tasks: nextTasks, renderOrder: nextOrder };
|
|
158
202
|
});
|
|
159
203
|
yield* markDirty;
|
|
160
204
|
|
|
@@ -162,25 +206,19 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
162
206
|
});
|
|
163
207
|
|
|
164
208
|
const updateTask = (taskId: TaskId, options: UpdateTaskOptions) =>
|
|
165
|
-
Ref.update(
|
|
166
|
-
const snapshot = tasks.get(taskId);
|
|
167
|
-
if (!snapshot)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const next = new Map(tasks);
|
|
172
|
-
next.set(taskId, updatedSnapshot(snapshot, options));
|
|
173
|
-
return next;
|
|
209
|
+
Ref.update(storeRef, (store) => {
|
|
210
|
+
const snapshot = store.tasks.get(taskId);
|
|
211
|
+
if (!snapshot) return store;
|
|
212
|
+
const nextTasks = new Map(store.tasks);
|
|
213
|
+
nextTasks.set(taskId, updatedSnapshot(snapshot, options));
|
|
214
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
174
215
|
}).pipe(Effect.zipRight(markDirty));
|
|
175
216
|
|
|
176
217
|
const advanceTask = (taskId: TaskId, amount = 1) =>
|
|
177
|
-
Ref.update(
|
|
178
|
-
const snapshot = tasks.get(taskId);
|
|
179
|
-
if (!snapshot)
|
|
180
|
-
return tasks;
|
|
181
|
-
}
|
|
218
|
+
Ref.update(storeRef, (store) => {
|
|
219
|
+
const snapshot = store.tasks.get(taskId);
|
|
220
|
+
if (!snapshot) return store;
|
|
182
221
|
|
|
183
|
-
const next = new Map(tasks);
|
|
184
222
|
const units =
|
|
185
223
|
snapshot.units._tag === "DeterminateTaskUnits"
|
|
186
224
|
? new DeterminateTaskUnits({
|
|
@@ -191,7 +229,8 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
191
229
|
spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount),
|
|
192
230
|
});
|
|
193
231
|
|
|
194
|
-
|
|
232
|
+
const nextTasks = new Map(store.tasks);
|
|
233
|
+
nextTasks.set(
|
|
195
234
|
taskId,
|
|
196
235
|
new TaskSnapshot({
|
|
197
236
|
id: snapshot.id,
|
|
@@ -200,27 +239,25 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
200
239
|
status: snapshot.status,
|
|
201
240
|
transient: snapshot.transient,
|
|
202
241
|
units,
|
|
203
|
-
|
|
242
|
+
config: snapshot.config,
|
|
204
243
|
}),
|
|
205
244
|
);
|
|
206
245
|
|
|
207
|
-
return
|
|
246
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
208
247
|
}).pipe(Effect.zipRight(markDirty));
|
|
209
248
|
|
|
210
249
|
const completeTask = (taskId: TaskId) =>
|
|
211
|
-
Ref.update(
|
|
212
|
-
const snapshot = tasks.get(taskId);
|
|
213
|
-
if (!snapshot)
|
|
214
|
-
return tasks;
|
|
215
|
-
}
|
|
250
|
+
Ref.update(storeRef, (store) => {
|
|
251
|
+
const snapshot = store.tasks.get(taskId);
|
|
252
|
+
if (!snapshot) return store;
|
|
216
253
|
|
|
217
|
-
const
|
|
254
|
+
const nextTasks = new Map(store.tasks);
|
|
218
255
|
if (snapshot.transient) {
|
|
219
|
-
|
|
220
|
-
return
|
|
256
|
+
nextTasks.delete(taskId);
|
|
257
|
+
return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
|
|
221
258
|
}
|
|
222
259
|
|
|
223
|
-
|
|
260
|
+
nextTasks.set(
|
|
224
261
|
taskId,
|
|
225
262
|
new TaskSnapshot({
|
|
226
263
|
id: snapshot.id,
|
|
@@ -235,26 +272,24 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
235
272
|
total: snapshot.units.total,
|
|
236
273
|
})
|
|
237
274
|
: snapshot.units,
|
|
238
|
-
|
|
275
|
+
config: snapshot.config,
|
|
239
276
|
}),
|
|
240
277
|
);
|
|
241
|
-
return
|
|
278
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
242
279
|
}).pipe(Effect.zipRight(markDirty));
|
|
243
280
|
|
|
244
281
|
const failTask = (taskId: TaskId) =>
|
|
245
|
-
Ref.update(
|
|
246
|
-
const snapshot = tasks.get(taskId);
|
|
247
|
-
if (!snapshot)
|
|
248
|
-
return tasks;
|
|
249
|
-
}
|
|
282
|
+
Ref.update(storeRef, (store) => {
|
|
283
|
+
const snapshot = store.tasks.get(taskId);
|
|
284
|
+
if (!snapshot) return store;
|
|
250
285
|
|
|
251
|
-
const
|
|
286
|
+
const nextTasks = new Map(store.tasks);
|
|
252
287
|
if (snapshot.transient) {
|
|
253
|
-
|
|
254
|
-
return
|
|
288
|
+
nextTasks.delete(taskId);
|
|
289
|
+
return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
|
|
255
290
|
}
|
|
256
291
|
|
|
257
|
-
|
|
292
|
+
nextTasks.set(
|
|
258
293
|
taskId,
|
|
259
294
|
new TaskSnapshot({
|
|
260
295
|
id: snapshot.id,
|
|
@@ -263,10 +298,10 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
263
298
|
status: "failed",
|
|
264
299
|
transient: snapshot.transient,
|
|
265
300
|
units: snapshot.units,
|
|
266
|
-
|
|
301
|
+
config: snapshot.config,
|
|
267
302
|
}),
|
|
268
303
|
);
|
|
269
|
-
return
|
|
304
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
270
305
|
}).pipe(Effect.zipRight(markDirty));
|
|
271
306
|
|
|
272
307
|
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
@@ -275,6 +310,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
275
310
|
return;
|
|
276
311
|
}
|
|
277
312
|
|
|
313
|
+
// TODO: Might wanna replace this or make it configurable. Look for other options.
|
|
278
314
|
const message = formatWithOptions(
|
|
279
315
|
{
|
|
280
316
|
colors: isTTY,
|
|
@@ -297,67 +333,61 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
297
333
|
yield* markDirty;
|
|
298
334
|
});
|
|
299
335
|
|
|
300
|
-
const log = (...args: ReadonlyArray<unknown>) =>
|
|
301
|
-
Effect.gen(function* () {
|
|
302
|
-
yield* appendLog(args);
|
|
303
|
-
});
|
|
336
|
+
const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
|
|
304
337
|
|
|
305
338
|
const getTask = (taskId: TaskId) =>
|
|
306
|
-
Ref.get(
|
|
339
|
+
Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
|
|
307
340
|
|
|
308
|
-
const listTasks = Ref.get(
|
|
341
|
+
const listTasks = Ref.get(storeRef).pipe(Effect.map((store) => Array.from(store.tasks.values())));
|
|
309
342
|
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
343
|
+
const runTask: ProgressService["runTask"] = dual(
|
|
344
|
+
2,
|
|
345
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
346
|
+
Effect.gen(function* () {
|
|
347
|
+
const outerConsole = yield* Effect.console;
|
|
348
|
+
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
349
|
+
const resolvedParentId =
|
|
350
|
+
options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
|
|
351
|
+
|
|
352
|
+
const taskId = yield* addTask({
|
|
353
|
+
...options,
|
|
354
|
+
parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : undefined,
|
|
355
|
+
transient: options.transient ?? Option.isSome(resolvedParentId),
|
|
356
|
+
});
|
|
322
357
|
|
|
323
|
-
|
|
324
|
-
Effect.locally(
|
|
358
|
+
return yield* Effect.locally(
|
|
325
359
|
Effect.withConsole(
|
|
326
360
|
Effect.provideService(effect, Task, taskId),
|
|
327
361
|
makeProgressConsole(log, outerConsole),
|
|
328
362
|
),
|
|
329
363
|
currentParentRef,
|
|
330
364
|
Option.some(taskId),
|
|
331
|
-
),
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
if (Exit.isSuccess(exit)) {
|
|
335
|
-
yield* completeTask(taskId);
|
|
336
|
-
} else {
|
|
337
|
-
yield* failTask(taskId);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return yield* Exit.match(exit, {
|
|
341
|
-
onFailure: Effect.failCause,
|
|
342
|
-
onSuccess: Effect.succeed,
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const trackIterable: ProgressService["trackIterable"] = (iterable, options, f) =>
|
|
347
|
-
withTask(
|
|
348
|
-
{
|
|
349
|
-
description: options.description,
|
|
350
|
-
total: options.total ?? inferTotal(iterable),
|
|
351
|
-
transient: options.transient,
|
|
352
|
-
progressbar: options.progressbar,
|
|
353
|
-
},
|
|
354
|
-
Effect.gen(function* () {
|
|
355
|
-
const taskId = yield* Task;
|
|
356
|
-
return yield* Effect.forEach(iterable, (item, index) =>
|
|
357
|
-
Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
|
|
358
365
|
);
|
|
359
366
|
}),
|
|
360
|
-
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
const withTask: ProgressService["withTask"] = dual(
|
|
370
|
+
2,
|
|
371
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
372
|
+
runTask(
|
|
373
|
+
Effect.gen(function* () {
|
|
374
|
+
const taskId = yield* Task;
|
|
375
|
+
const exit = yield* Effect.exit(effect);
|
|
376
|
+
|
|
377
|
+
if (Exit.isSuccess(exit)) {
|
|
378
|
+
yield* completeTask(taskId);
|
|
379
|
+
} else {
|
|
380
|
+
yield* failTask(taskId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return yield* Exit.match(exit, {
|
|
384
|
+
onFailure: Effect.failCause,
|
|
385
|
+
onSuccess: Effect.succeed,
|
|
386
|
+
});
|
|
387
|
+
}),
|
|
388
|
+
options,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
361
391
|
|
|
362
392
|
const service: ProgressService = {
|
|
363
393
|
addTask,
|
|
@@ -368,8 +398,8 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
368
398
|
log,
|
|
369
399
|
getTask,
|
|
370
400
|
listTasks,
|
|
401
|
+
runTask,
|
|
371
402
|
withTask,
|
|
372
|
-
trackIterable,
|
|
373
403
|
};
|
|
374
404
|
|
|
375
405
|
return Progress.of(service);
|
package/src/terminal.ts
CHANGED
|
@@ -5,9 +5,7 @@ export interface ProgressTerminalService {
|
|
|
5
5
|
readonly stderrRows: Effect.Effect<number | undefined>;
|
|
6
6
|
readonly stderrColumns: Effect.Effect<number | undefined>;
|
|
7
7
|
readonly writeStderr: (text: string) => Effect.Effect<void>;
|
|
8
|
-
readonly withRawInputCapture: <A, E, R>(
|
|
9
|
-
effect: Effect.Effect<A, E, R>,
|
|
10
|
-
) => Effect.Effect<A, E, R>;
|
|
8
|
+
readonly withRawInputCapture: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
11
9
|
}
|
|
12
10
|
|
|
13
11
|
const withRawInputCapture: ProgressTerminalService["withRawInputCapture"] = (effect) =>
|
package/src/types.ts
CHANGED
|
@@ -102,9 +102,19 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
|
|
|
102
102
|
status: TaskStatusSchema,
|
|
103
103
|
transient: Schema.Boolean,
|
|
104
104
|
units: TaskUnitsSchema,
|
|
105
|
-
|
|
105
|
+
config: ProgressBarConfigSchema,
|
|
106
106
|
}) {}
|
|
107
107
|
|
|
108
|
+
export interface RenderRow {
|
|
109
|
+
readonly id: TaskId;
|
|
110
|
+
readonly depth: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface TaskStore {
|
|
114
|
+
readonly tasks: Map<TaskId, TaskSnapshot>;
|
|
115
|
+
readonly renderOrder: ReadonlyArray<RenderRow>;
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
export interface ProgressService {
|
|
109
119
|
readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
|
|
110
120
|
readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
|
|
@@ -114,15 +124,24 @@ export interface ProgressService {
|
|
|
114
124
|
readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
|
|
115
125
|
readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
|
|
116
126
|
readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
|
|
117
|
-
readonly
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
readonly runTask: {
|
|
128
|
+
<A, E, R>(
|
|
129
|
+
effect: Effect.Effect<A, E, R>,
|
|
130
|
+
options: AddTaskOptions,
|
|
131
|
+
): Effect.Effect<A, E, Exclude<R, Task>>;
|
|
132
|
+
<A, E, R>(
|
|
133
|
+
options: AddTaskOptions,
|
|
134
|
+
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
|
|
135
|
+
};
|
|
136
|
+
readonly withTask: {
|
|
137
|
+
<A, E, R>(
|
|
138
|
+
effect: Effect.Effect<A, E, R>,
|
|
139
|
+
options: AddTaskOptions,
|
|
140
|
+
): Effect.Effect<A, E, Exclude<R, Task>>;
|
|
141
|
+
<A, E, R>(
|
|
142
|
+
options: AddTaskOptions,
|
|
143
|
+
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
|
|
144
|
+
};
|
|
126
145
|
}
|
|
127
146
|
|
|
128
147
|
export class Task extends Context.Tag("stromseng.dev/Task")<Task, TaskId>() {}
|