effective-progress 0.2.2 → 0.2.4

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
@@ -212,7 +212,7 @@ Supported modifiers:
212
212
 
213
213
  ## Dependencies & package size
214
214
 
215
- This library is designed for CLI workflows, where package size is typically a lower-priority concern. Alongside `effect`, it relies on two mature runtime dependencies: `chalk` and `es-toolkit`.
215
+ This library is designed for CLI workflows, where package size is typically a lower-priority concern. Alongside `effect` though, I will strive to only rely on other high quality packages.
216
216
 
217
217
  ## Notes
218
218
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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
@@ -19,11 +19,15 @@ export interface EffectAllExecutionOptions extends EffectExecutionOptions {
19
19
 
20
20
  export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
21
21
  export type AllReturn<
22
- Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
22
+ Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>,
23
23
  O extends EffectAllExecutionOptions,
24
- > = [Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends [
25
- Effect.Effect<infer A, infer E, infer R>,
26
- ]
24
+ > = [
25
+ [Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>]
26
+ ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
27
+ : [Arg] extends [Record<string, Effect.Effect<any, any, any>>]
28
+ ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
29
+ : never,
30
+ ] extends [Effect.Effect<infer A, infer E, infer R>]
27
31
  ? Effect.Effect<A, E, Exclude<R, Progress | Task>>
28
32
  : never;
29
33
 
@@ -52,25 +56,30 @@ export const task: {
52
56
  ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
53
57
  );
54
58
 
59
+ type AllArg = ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>;
60
+
61
+ const wrapEffects = (
62
+ effects: AllArg,
63
+ tap: (effect: Effect.Effect<any, any, any>) => Effect.Effect<any, any, any>,
64
+ ): AllArg =>
65
+ Array.isArray(effects)
66
+ ? effects.map(tap)
67
+ : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
68
+
69
+ const countEffects = (effects: AllArg): number =>
70
+ Array.isArray(effects) ? effects.length : Object.keys(effects).length;
71
+
55
72
  export const all: {
56
- <
57
- const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
58
- O extends EffectAllExecutionOptions,
59
- >(
73
+ <const Arg extends AllArg, O extends EffectAllExecutionOptions>(
60
74
  effects: Arg,
61
75
  options: Omit<TrackOptions, "total"> & O,
62
76
  ): AllReturn<Arg, O>;
63
77
  <O extends EffectAllExecutionOptions>(
64
78
  options: Omit<TrackOptions, "total"> & O,
65
- ): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(
66
- effects: Arg,
67
- ) => AllReturn<Arg, O>;
79
+ ): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
68
80
  } = dual(
69
81
  2,
70
- <
71
- const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
72
- O extends EffectAllExecutionOptions,
73
- >(
82
+ <const Arg extends AllArg, O extends EffectAllExecutionOptions>(
74
83
  effects: Arg,
75
84
  options: Omit<TrackOptions, "total"> & O,
76
85
  ) =>
@@ -82,7 +91,9 @@ export const all: {
82
91
  const taskId = yield* Task;
83
92
  const exit = yield* Effect.exit(
84
93
  Effect.all(
85
- effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
94
+ wrapEffects(effects, (effect) =>
95
+ Effect.tap(effect, () => progress.advanceTask(taskId, 1)),
96
+ ),
86
97
  {
87
98
  concurrency: options.concurrency,
88
99
  batching: options.batching,
@@ -106,7 +117,7 @@ export const all: {
106
117
  }),
107
118
  {
108
119
  description: options.description,
109
- total: effects.length,
120
+ total: countEffects(effects),
110
121
  transient: options.transient,
111
122
  progressbar: options.progressbar,
112
123
  },
package/src/colors.ts CHANGED
@@ -104,10 +104,10 @@ export const defaultProgressBarColors: ProgressBarColors = {
104
104
  };
105
105
 
106
106
  const applyModifier = (instance: ChalkInstance, modifier: StyleModifier): ChalkInstance =>
107
- instance[modifier] as ChalkInstance;
107
+ instance[modifier];
108
108
 
109
109
  const applyNamedColor = (instance: ChalkInstance, color: NamedColor): ChalkInstance =>
110
- instance[color] as ChalkInstance;
110
+ instance[color];
111
111
 
112
112
  const resolveBaseStyle = (style: ColorStyle): ChalkInstance => {
113
113
  switch (style.kind) {
package/src/renderer.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect, Ref, Schema } from "effect";
1
+ import { Clock, Duration, Effect, Ref, Schema } from "effect";
2
2
  import {
3
3
  type CompiledProgressBarColors,
4
4
  compileProgressBarColors,
@@ -27,34 +27,45 @@ const renderDeterminate = (
27
27
  return `${colors.brackets(progressbar.leftBracket)}${bar}${colors.brackets(progressbar.rightBracket)} ${units.completed}/${units.total} ${colors.percent(percent + "%")}`;
28
28
  };
29
29
 
30
+ const formatElapsed = (snapshot: TaskSnapshot, now: number): string => {
31
+ const elapsedMillis = (snapshot.completedAt ?? now) - snapshot.startedAt;
32
+ const duration =
33
+ snapshot.status === "running"
34
+ ? Duration.seconds(Math.floor(elapsedMillis / 1000))
35
+ : Duration.millis(elapsedMillis);
36
+ return ` (${Duration.format(duration)})`;
37
+ };
38
+
30
39
  const buildTaskLine = (
31
40
  snapshot: TaskSnapshot,
32
41
  depth: number,
33
42
  tick: number,
34
43
  colors: CompiledProgressBarColors,
44
+ now: number,
35
45
  ): string => {
36
46
  const progressbar = snapshot.config;
37
47
  const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
48
+ const elapsed = formatElapsed(snapshot, now);
38
49
 
39
50
  if (snapshot.status === "failed") {
40
- return `${prefix}${colors.failed("[failed]")}`;
51
+ return `${prefix}${colors.failed("[failed]")}${elapsed}`;
41
52
  }
42
53
 
43
54
  if (snapshot.status === "done") {
44
55
  if (snapshot.units._tag === "DeterminateTaskUnits") {
45
- return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}`;
56
+ return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}${elapsed}`;
46
57
  }
47
- return `${prefix}${colors.done("[done]")}`;
58
+ return `${prefix}${colors.done("[done]")}${elapsed}`;
48
59
  }
49
60
 
50
61
  if (snapshot.units._tag === "DeterminateTaskUnits") {
51
- return prefix + renderDeterminate(snapshot.units, progressbar, colors);
62
+ return prefix + renderDeterminate(snapshot.units, progressbar, colors) + elapsed;
52
63
  }
53
64
 
54
65
  const frames = progressbar.spinnerFrames;
55
66
  const frameIndex = (snapshot.units.spinnerFrame + tick) % frames.length;
56
67
  const frame = frames[frameIndex] ?? frames[0]!;
57
- return `${prefix}${colors.spinner(frame)}`;
68
+ return `${prefix}${colors.spinner(frame)}${elapsed}`;
58
69
  };
59
70
 
60
71
  export const runProgressServiceRenderer = (
@@ -170,10 +181,11 @@ export const runProgressServiceRenderer = (
170
181
  if (!snapshot || (snapshot.transient && snapshot.status !== "running")) return [];
171
182
  return [{ snapshot, depth: row.depth }];
172
183
  });
184
+ const now = yield* Clock.currentTimeMillis;
173
185
  const frameTick = mode === "final" ? tick + 1 : tick;
174
186
  const taskLines = ordered.map(({ snapshot, depth }) => {
175
187
  const lineTick = isTTY ? frameTick : 0;
176
- return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.config));
188
+ return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.config), now);
177
189
  });
178
190
 
179
191
  if (isTTY) {
package/src/runtime.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
1
+ import { Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
2
2
  import { dual } from "effect/Function";
3
3
  import { mergeWith } from "es-toolkit/object";
4
4
  import { formatWithOptions } from "node:util";
@@ -82,6 +82,8 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
82
82
  transient: options.transient ?? snapshot.transient,
83
83
  units,
84
84
  config: snapshot.config,
85
+ startedAt: snapshot.startedAt,
86
+ completedAt: snapshot.completedAt,
85
87
  });
86
88
  };
87
89
 
@@ -181,6 +183,7 @@ const makeProgressService = Effect.gen(function* () {
181
183
  mergeConfig(inheritedProgressBarConfig, options.progressbar),
182
184
  );
183
185
 
186
+ const now = yield* Clock.currentTimeMillis;
184
187
  const parentIdValue = Option.getOrNull(resolvedParentId);
185
188
  const snapshot = new TaskSnapshot({
186
189
  id: taskId,
@@ -190,6 +193,8 @@ const makeProgressService = Effect.gen(function* () {
190
193
  transient: options.transient ?? false,
191
194
  units,
192
195
  config: resolvedProgressBarConfig,
196
+ startedAt: now,
197
+ completedAt: null,
193
198
  });
194
199
 
195
200
  yield* Ref.update(storeRef, (s) => {
@@ -240,6 +245,8 @@ const makeProgressService = Effect.gen(function* () {
240
245
  transient: snapshot.transient,
241
246
  units,
242
247
  config: snapshot.config,
248
+ startedAt: snapshot.startedAt,
249
+ completedAt: snapshot.completedAt,
243
250
  }),
244
251
  );
245
252
 
@@ -247,62 +254,74 @@ const makeProgressService = Effect.gen(function* () {
247
254
  }).pipe(Effect.zipRight(markDirty));
248
255
 
249
256
  const completeTask = (taskId: TaskId) =>
250
- Ref.update(storeRef, (store) => {
251
- const snapshot = store.tasks.get(taskId);
252
- if (!snapshot) return store;
253
-
254
- const nextTasks = new Map(store.tasks);
255
- if (snapshot.transient) {
256
- nextTasks.delete(taskId);
257
- return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
258
- }
259
-
260
- nextTasks.set(
261
- taskId,
262
- new TaskSnapshot({
263
- id: snapshot.id,
264
- parentId: snapshot.parentId,
265
- description: snapshot.description,
266
- status: "done",
267
- transient: snapshot.transient,
268
- units:
269
- snapshot.units._tag === "DeterminateTaskUnits"
270
- ? new DeterminateTaskUnits({
271
- completed: snapshot.units.total,
272
- total: snapshot.units.total,
273
- })
274
- : snapshot.units,
275
- config: snapshot.config,
276
- }),
277
- );
278
- return { tasks: nextTasks, renderOrder: store.renderOrder };
279
- }).pipe(Effect.zipRight(markDirty));
257
+ Effect.gen(function* () {
258
+ const now = yield* Clock.currentTimeMillis;
259
+ yield* Ref.update(storeRef, (store) => {
260
+ const snapshot = store.tasks.get(taskId);
261
+ if (!snapshot) return store;
262
+
263
+ const nextTasks = new Map(store.tasks);
264
+ if (snapshot.transient) {
265
+ nextTasks.delete(taskId);
266
+ return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
267
+ }
268
+
269
+ nextTasks.set(
270
+ taskId,
271
+ new TaskSnapshot({
272
+ id: snapshot.id,
273
+ parentId: snapshot.parentId,
274
+ description: snapshot.description,
275
+ status: "done",
276
+ transient: snapshot.transient,
277
+ units:
278
+ snapshot.units._tag === "DeterminateTaskUnits"
279
+ ? new DeterminateTaskUnits({
280
+ completed: snapshot.units.total,
281
+ total: snapshot.units.total,
282
+ })
283
+ : snapshot.units,
284
+ config: snapshot.config,
285
+ startedAt: snapshot.startedAt,
286
+ completedAt: now,
287
+ }),
288
+ );
289
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
290
+ });
291
+ yield* markDirty;
292
+ });
280
293
 
281
294
  const failTask = (taskId: TaskId) =>
282
- Ref.update(storeRef, (store) => {
283
- const snapshot = store.tasks.get(taskId);
284
- if (!snapshot) return store;
285
-
286
- const nextTasks = new Map(store.tasks);
287
- if (snapshot.transient) {
288
- nextTasks.delete(taskId);
289
- return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
290
- }
291
-
292
- nextTasks.set(
293
- taskId,
294
- new TaskSnapshot({
295
- id: snapshot.id,
296
- parentId: snapshot.parentId,
297
- description: snapshot.description,
298
- status: "failed",
299
- transient: snapshot.transient,
300
- units: snapshot.units,
301
- config: snapshot.config,
302
- }),
303
- );
304
- return { tasks: nextTasks, renderOrder: store.renderOrder };
305
- }).pipe(Effect.zipRight(markDirty));
295
+ Effect.gen(function* () {
296
+ const now = yield* Clock.currentTimeMillis;
297
+ yield* Ref.update(storeRef, (store) => {
298
+ const snapshot = store.tasks.get(taskId);
299
+ if (!snapshot) return store;
300
+
301
+ const nextTasks = new Map(store.tasks);
302
+ if (snapshot.transient) {
303
+ nextTasks.delete(taskId);
304
+ return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
305
+ }
306
+
307
+ nextTasks.set(
308
+ taskId,
309
+ new TaskSnapshot({
310
+ id: snapshot.id,
311
+ parentId: snapshot.parentId,
312
+ description: snapshot.description,
313
+ status: "failed",
314
+ transient: snapshot.transient,
315
+ units: snapshot.units,
316
+ config: snapshot.config,
317
+ startedAt: snapshot.startedAt,
318
+ completedAt: now,
319
+ }),
320
+ );
321
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
322
+ });
323
+ yield* markDirty;
324
+ });
306
325
 
307
326
  const appendLog = (args: ReadonlyArray<unknown>) =>
308
327
  Effect.gen(function* () {
package/src/types.ts CHANGED
@@ -25,7 +25,7 @@ export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarC
25
25
 
26
26
  export const defaultRendererConfig: RendererConfigShape = {
27
27
  disableUserInput: true,
28
- renderIntervalMillis: 50, // 20 FPS
28
+ renderIntervalMillis: 100, // 10 FPS
29
29
  maxLogLines: 0,
30
30
  nonTtyUpdateStep: 5,
31
31
  };
@@ -103,6 +103,8 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
103
103
  transient: Schema.Boolean,
104
104
  units: TaskUnitsSchema,
105
105
  config: ProgressBarConfigSchema,
106
+ startedAt: Schema.Number,
107
+ completedAt: Schema.NullOr(Schema.Number),
106
108
  }) {}
107
109
 
108
110
  export interface RenderRow {