effective-progress 0.2.0 → 0.2.1

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/effective-progress)](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,25 @@ 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(
123
+ Effect.sleep("1 second"),
124
+ {
125
+ description: "Worker pipeline",
126
+ progressbar: {
127
+ barWidth: 20,
128
+ spinnerFrames: [".", "o", "O", "0"],
129
+ colors: {
130
+ spinner: { kind: "named", value: "magentaBright" },
131
+ },
132
+ },
133
+ },
134
+ );
135
+ ```
136
+
118
137
  ## Terminal service and mocking
119
138
 
120
139
  `effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
@@ -125,7 +144,7 @@ Effect.runPromise(configured);
125
144
  - `writeStderr(text)`
126
145
  - `withRawInputCapture(effect)`
127
146
 
128
- You can provide a mock in tests instead of monkeypatching global process streams:
147
+ 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
148
 
130
149
  ```ts
131
150
  import { Effect } from "effect";
@@ -139,45 +158,23 @@ const mockTerminal: Progress.ProgressTerminalService = {
139
158
  withRawInputCapture: (effect) => effect,
140
159
  };
141
160
 
142
- const program = Progress.withTask({ description: "work" }, Effect.sleep("100 millis")).pipe(
161
+ const program = Progress.withTask(Effect.sleep("100 millis"), { description: "work" }).pipe(
143
162
  Effect.provideService(Progress.ProgressTerminal, mockTerminal),
144
163
  );
145
164
  ```
146
165
 
147
- ## Migration note
148
-
149
- `RendererConfig.isTTY` has been removed.
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):
153
-
154
- ```ts
155
- yield *
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
- ```
166
+ ## Manual task control
170
167
 
171
168
  For manual usage, `withTask` captures logs implicitly and provides the current `Task` context:
172
169
 
173
170
  ```ts
174
171
  const program = Progress.withTask(
175
- { description: "Manual task" },
176
172
  Effect.gen(function* () {
177
173
  const currentTask = yield* Progress.Task;
178
174
  yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
179
175
  yield* Effect.sleep("1 second");
180
176
  }),
177
+ { description: "Manual task" },
181
178
  );
182
179
  ```
183
180
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Effect-first terminal progress bars with nested multibar support",
5
5
  "homepage": "https://github.com/stromseng/effective-progress#readme",
6
6
  "bugs": {
package/src/api.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Effect } 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";
@@ -31,81 +32,113 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
31
32
 
32
33
  export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
33
34
 
34
- export const withTask = <A, E, R>(
35
- options: AddTaskOptions,
36
- effect: Effect.Effect<A, E, R>,
37
- ): Effect.Effect<A, E, Exclude<R, Progress | Task>> =>
35
+ export const withTask: {
36
+ <A, E, R>(
37
+ effect: Effect.Effect<A, E, R>,
38
+ options: AddTaskOptions,
39
+ ): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
40
+ <A, E, R>(
41
+ options: AddTaskOptions,
42
+ ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
43
+ } = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
38
44
  provideProgressService(
39
45
  Effect.gen(function* () {
40
46
  const progress = yield* Progress;
41
- return yield* progress.withTask(options, effect);
47
+ return yield* progress.withTask(effect, options);
42
48
  }),
43
- ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>;
49
+ ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
50
+ );
44
51
 
45
- export const all = <
46
- const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
47
- O extends EffectAllExecutionOptions,
48
- >(
49
- effects: Arg,
50
- options: Omit<TrackOptions, "total"> & O,
51
- ): AllReturn<Arg, O> =>
52
- provideProgressService(
53
- Effect.gen(function* () {
54
- const progress = yield* Progress;
55
- return yield* progress.withTask(
56
- {
57
- description: options.description,
58
- total: effects.length,
59
- transient: options.transient,
60
- progressbar: options.progressbar,
61
- },
62
- Effect.gen(function* () {
63
- const taskId = yield* Task;
64
- return yield* Effect.all(
65
- effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
66
- {
67
- concurrency: options.concurrency,
68
- batching: options.batching,
69
- discard: options.discard,
70
- mode: options.mode,
71
- concurrentFinalizers: options.concurrentFinalizers,
72
- },
73
- );
74
- }),
75
- );
76
- }),
77
- ) as AllReturn<Arg, O>;
52
+ export const all: {
53
+ <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
54
+ effects: Arg,
55
+ options: Omit<TrackOptions, "total"> & O,
56
+ ): AllReturn<Arg, O>;
57
+ <O extends EffectAllExecutionOptions>(
58
+ options: Omit<TrackOptions, "total"> & O,
59
+ ): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(effects: Arg) => AllReturn<
60
+ Arg,
61
+ O
62
+ >;
63
+ } = dual(
64
+ 2,
65
+ <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
66
+ effects: Arg,
67
+ options: Omit<TrackOptions, "total"> & O,
68
+ ) =>
69
+ provideProgressService(
70
+ Effect.gen(function* () {
71
+ const progress = yield* Progress;
72
+ return yield* progress.withTask(
73
+ Effect.gen(function* () {
74
+ const taskId = yield* Task;
75
+ return yield* Effect.all(
76
+ effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
77
+ {
78
+ concurrency: options.concurrency,
79
+ batching: options.batching,
80
+ discard: options.discard,
81
+ mode: options.mode,
82
+ concurrentFinalizers: options.concurrentFinalizers,
83
+ },
84
+ );
85
+ }),
86
+ {
87
+ description: options.description,
88
+ total: effects.length,
89
+ transient: options.transient,
90
+ progressbar: options.progressbar,
91
+ },
92
+ );
93
+ }),
94
+ ) as AllReturn<Arg, O>,
95
+ );
78
96
 
79
- export const forEach = <A, B, E, R>(
80
- iterable: Iterable<A>,
81
- f: (item: A, index: number) => Effect.Effect<B, E, R>,
82
- options: ForEachOptions,
83
- ): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>> =>
84
- provideProgressService(
85
- Effect.gen(function* () {
86
- const progress = yield* Progress;
97
+ export const forEach: {
98
+ <A, B, E, R>(
99
+ iterable: Iterable<A>,
100
+ f: (item: A, index: number) => Effect.Effect<B, E, R>,
101
+ options: ForEachOptions,
102
+ ): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
103
+ <A, B, E, R>(
104
+ f: (item: A, index: number) => Effect.Effect<B, E, R>,
105
+ options: ForEachOptions,
106
+ ): (
107
+ iterable: Iterable<A>,
108
+ ) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
109
+ } = dual(
110
+ 3,
111
+ <A, B, E, R>(
112
+ iterable: Iterable<A>,
113
+ f: (item: A, index: number) => Effect.Effect<B, E, R>,
114
+ options: ForEachOptions,
115
+ ) =>
116
+ provideProgressService(
117
+ Effect.gen(function* () {
118
+ const progress = yield* Progress;
87
119
 
88
- return yield* progress.withTask(
89
- {
90
- description: options.description,
91
- total: options.total ?? inferTotal(iterable),
92
- transient: options.transient,
93
- progressbar: options.progressbar,
94
- },
95
- Effect.gen(function* () {
96
- const taskId = yield* Task;
97
- return yield* Effect.forEach(
98
- iterable,
99
- (item, index) =>
100
- Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
101
- {
102
- concurrency: options.concurrency,
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>>;
120
+ return yield* progress.withTask(
121
+ Effect.gen(function* () {
122
+ const taskId = yield* Task;
123
+ return yield* Effect.forEach(
124
+ iterable,
125
+ (item, index) =>
126
+ Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
127
+ {
128
+ concurrency: options.concurrency,
129
+ batching: options.batching,
130
+ discard: options.discard,
131
+ concurrentFinalizers: options.concurrentFinalizers,
132
+ },
133
+ );
134
+ }),
135
+ {
136
+ description: options.description,
137
+ total: options.total ?? inferTotal(iterable),
138
+ transient: options.transient,
139
+ progressbar: options.progressbar,
140
+ },
141
+ );
142
+ }),
143
+ ) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>,
144
+ );
package/src/runtime.ts CHANGED
@@ -1,4 +1,5 @@
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";
@@ -275,6 +276,7 @@ const makeProgressService = Effect.gen(function* () {
275
276
  return;
276
277
  }
277
278
 
279
+ // TODO: Might wanna replace this or make it configurable. Look for other options.
278
280
  const message = formatWithOptions(
279
281
  {
280
282
  colors: isTTY,
@@ -297,66 +299,66 @@ const makeProgressService = Effect.gen(function* () {
297
299
  yield* markDirty;
298
300
  });
299
301
 
300
- const log = (...args: ReadonlyArray<unknown>) =>
301
- Effect.gen(function* () {
302
- yield* appendLog(args);
303
- });
302
+ const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
304
303
 
305
304
  const getTask = (taskId: TaskId) =>
306
305
  Ref.get(tasksRef).pipe(Effect.map((tasks) => Option.fromNullable(tasks.get(taskId))));
307
306
 
308
307
  const listTasks = Ref.get(tasksRef).pipe(Effect.map((tasks) => Array.from(tasks.values())));
309
308
 
310
- const withTask: ProgressService["withTask"] = (options, effect) =>
311
- Effect.gen(function* () {
312
- const outerConsole = yield* Effect.console;
313
- const inheritedParentId = yield* FiberRef.get(currentParentRef);
314
- const resolvedParentId =
315
- options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
316
-
317
- const taskId = yield* addTask({
318
- ...options,
319
- parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : undefined,
320
- transient: options.transient ?? Option.isSome(resolvedParentId),
321
- });
309
+ const withTask: ProgressService["withTask"] = dual(
310
+ 2,
311
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
312
+ Effect.gen(function* () {
313
+ const outerConsole = yield* Effect.console;
314
+ const inheritedParentId = yield* FiberRef.get(currentParentRef);
315
+ const resolvedParentId =
316
+ options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
317
+
318
+ const taskId = yield* addTask({
319
+ ...options,
320
+ parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : undefined,
321
+ transient: options.transient ?? Option.isSome(resolvedParentId),
322
+ });
322
323
 
323
- const exit = yield* Effect.exit(
324
- Effect.locally(
325
- Effect.withConsole(
326
- Effect.provideService(effect, Task, taskId),
327
- makeProgressConsole(log, outerConsole),
324
+ const exit = yield* Effect.exit(
325
+ Effect.locally(
326
+ Effect.withConsole(
327
+ Effect.provideService(effect, Task, taskId),
328
+ makeProgressConsole(log, outerConsole),
329
+ ),
330
+ currentParentRef,
331
+ Option.some(taskId),
328
332
  ),
329
- currentParentRef,
330
- Option.some(taskId),
331
- ),
332
- );
333
+ );
333
334
 
334
- if (Exit.isSuccess(exit)) {
335
- yield* completeTask(taskId);
336
- } else {
337
- yield* failTask(taskId);
338
- }
335
+ if (Exit.isSuccess(exit)) {
336
+ yield* completeTask(taskId);
337
+ } else {
338
+ yield* failTask(taskId);
339
+ }
339
340
 
340
- return yield* Exit.match(exit, {
341
- onFailure: Effect.failCause,
342
- onSuccess: Effect.succeed,
343
- });
344
- });
341
+ return yield* Exit.match(exit, {
342
+ onFailure: Effect.failCause,
343
+ onSuccess: Effect.succeed,
344
+ });
345
+ }),
346
+ );
345
347
 
346
348
  const trackIterable: ProgressService["trackIterable"] = (iterable, options, f) =>
347
349
  withTask(
348
- {
349
- description: options.description,
350
- total: options.total ?? inferTotal(iterable),
351
- transient: options.transient,
352
- progressbar: options.progressbar,
353
- },
354
350
  Effect.gen(function* () {
355
351
  const taskId = yield* Task;
356
352
  return yield* Effect.forEach(iterable, (item, index) =>
357
353
  Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
358
354
  );
359
355
  }),
356
+ {
357
+ description: options.description,
358
+ total: options.total ?? inferTotal(iterable),
359
+ transient: options.transient,
360
+ progressbar: options.progressbar,
361
+ },
360
362
  );
361
363
 
362
364
  const service: ProgressService = {
package/src/types.ts CHANGED
@@ -114,10 +114,15 @@ export interface ProgressService {
114
114
  readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
115
115
  readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
116
116
  readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
117
- readonly withTask: <A, E, R>(
118
- options: AddTaskOptions,
119
- effect: Effect.Effect<A, E, R>,
120
- ) => Effect.Effect<A, E, Exclude<R, Task>>;
117
+ readonly withTask: {
118
+ <A, E, R>(
119
+ effect: Effect.Effect<A, E, R>,
120
+ options: AddTaskOptions,
121
+ ): Effect.Effect<A, E, Exclude<R, Task>>;
122
+ <A, E, R>(
123
+ options: AddTaskOptions,
124
+ ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
125
+ };
121
126
  readonly trackIterable: <A, B, E, R>(
122
127
  iterable: Iterable<A>,
123
128
  options: TrackOptions,