effective-progress 0.6.0 → 0.6.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 CHANGED
@@ -17,7 +17,7 @@
17
17
  - spinner support for “we have no idea how long this takes” work
18
18
  - keep using `Console.log` / `Effect.logInfo` while progress rendering is active
19
19
  - familiar `.all` and `.forEach` APIs — swap `Effect` for `Progress`, get progress bars basically for free
20
- - flicker-free rendering (in theory) by drawing everything in a single terminal frame
20
+ - flicker-free rendering with [Ink](https://github.com/vadimdemedes/ink)
21
21
 
22
22
  ## Install
23
23
 
@@ -27,7 +27,7 @@ bun add effective-progress
27
27
 
28
28
  ## Usage
29
29
 
30
- This shows the simplest usage: iterate items with a single progress bar.
30
+ Iterate items with a single progress bar.
31
31
 
32
32
  ```ts
33
33
  import { Console, Effect } from "effect";
@@ -50,13 +50,7 @@ Effect.runPromise(program);
50
50
 
51
51
  ### Nested example
52
52
 
53
- Run:
54
-
55
- ```bash
56
- bun examples/nesting.ts
57
- ```
58
-
59
- This demonstrates nested multibar behavior where parent tasks each run their own child progress bars.
53
+ Nested progress bars with tree-style rendering that highlights parent tasks and their subtasks
60
54
 
61
55
  ```ts
62
56
  import { Effect } from "effect";
@@ -81,15 +75,21 @@ Effect.runPromise(program);
81
75
 
82
76
  ### Effect.all modes
83
77
 
84
- We support the `either`/`validate` modes of `Effect.all` and render the amount of sucesses/failures.
78
+ Support for `either`/`validate` modes of `Effect.all` and render the amount of sucesses/failures.
85
79
 
86
80
  <img alt="Mixed outcomes modes output" src="docs/images/mixedOutcomes.gif" width="600" />
87
81
 
82
+ - `Progress.all` in default mode (`mode: "default"`) remains fail-fast.
83
+ - In fail-fast runs, unresolved units remain unprocessed.
84
+ - `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
85
+ - Mixed outcomes can still finalize as `done` when all units are accounted for.
86
+
88
87
  ### Other examples
89
88
 
90
89
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
91
90
  - `examples/advancedExample.ts` - full API usage and manual task control
92
91
  - `examples/mixedOutcomes.ts` - fail-fast vs `either`/`validate` with mixed success/failure counters
92
+ - `examples/unknownTotalCounting.ts` - count successes/failures without a known total and render `processed/?`
93
93
  - `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
94
94
  - `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
95
95
 
@@ -112,13 +112,6 @@ We support the `either`/`validate` modes of `Effect.all` and render the amount o
112
112
  - Layout uses a 100-column baseline and grows when content requires more space.
113
113
  - On narrow terminals, layout compacts to fit available width and tree prefixes are suppressed when description space is too tight.
114
114
 
115
- ### Mixed outcomes and `mode`
116
-
117
- - `Progress.all` in default mode (`mode: "default"`) remains fail-fast.
118
- - In fail-fast runs, unresolved units remain unprocessed.
119
- - `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
120
- - Mixed outcomes can still finalize as `done` when all units are accounted for.
121
-
122
115
  ## Manual task control
123
116
 
124
117
  For manual usage, `task` still provides the current `Task` context, while logs continue through your outer `Console`:
@@ -131,8 +124,8 @@ const program = Progress.task(
131
124
  yield* Console.log("This log is handled by the outer Console", { taskId: currentTask });
132
125
 
133
126
  // Manual determinate updates:
134
- yield* progress.advanceTask(currentTask, 3);
135
- yield* progress.advanceTaskFailed(currentTask, 1);
127
+ yield* progress.incrementSucceeded(currentTask, 3);
128
+ yield* progress.incrementFailed(currentTask, 1);
136
129
  yield* Effect.sleep("1 second");
137
130
  }),
138
131
  { description: "Manual task", total: 10 },
package/dist/index.d.mts CHANGED
@@ -10,6 +10,13 @@ declare const TaskStatusSchema: Schema.Literal<["running", "done", "failed"]>;
10
10
  type TaskStatus = typeof TaskStatusSchema.Type;
11
11
  declare const TaskCountDisplaySchema: Schema.Literal<["processedOnly", "detailed"]>;
12
12
  type TaskCountDisplay = typeof TaskCountDisplaySchema.Type;
13
+ declare const InvalidTaskTotalError_base: Schema.TaggedErrorClass<InvalidTaskTotalError, "InvalidTaskTotalError", {
14
+ readonly _tag: Schema.tag<"InvalidTaskTotalError">;
15
+ } & {
16
+ total: typeof Schema.Number;
17
+ message: typeof Schema.String;
18
+ }>;
19
+ declare class InvalidTaskTotalError extends InvalidTaskTotalError_base {}
13
20
  interface AddTaskOptions {
14
21
  readonly description: string;
15
22
  readonly total?: number;
@@ -26,20 +33,12 @@ interface UpdateTaskOptions {
26
33
  readonly countDisplay?: TaskCountDisplay;
27
34
  }
28
35
  type TrackOptions = Exclude<AddTaskOptions, "parentId">;
29
- declare const DeterminateTaskUnits_base: Schema.TaggedClass<DeterminateTaskUnits, "DeterminateTaskUnits", {
30
- readonly _tag: Schema.tag<"DeterminateTaskUnits">;
31
- } & {
36
+ declare const TaskUnitsSchema: Schema.Struct<{
32
37
  succeeded: typeof Schema.Number;
33
38
  failed: typeof Schema.Number;
34
39
  processed: typeof Schema.Number;
35
- total: typeof Schema.Number;
36
- }>;
37
- declare class DeterminateTaskUnits extends DeterminateTaskUnits_base {}
38
- declare const IndeterminateTaskUnits_base: Schema.TaggedClass<IndeterminateTaskUnits, "IndeterminateTaskUnits", {
39
- readonly _tag: Schema.tag<"IndeterminateTaskUnits">;
40
+ total: Schema.optional<typeof Schema.Number>;
40
41
  }>;
41
- declare class IndeterminateTaskUnits extends IndeterminateTaskUnits_base {}
42
- declare const TaskUnitsSchema: Schema.Union<[typeof DeterminateTaskUnits, typeof IndeterminateTaskUnits]>;
43
42
  type TaskUnits = typeof TaskUnitsSchema.Type;
44
43
  declare const TaskSnapshot_base: Schema.TaggedClass<TaskSnapshot, "TaskSnapshot", {
45
44
  readonly _tag: Schema.tag<"TaskSnapshot">;
@@ -50,7 +49,12 @@ declare const TaskSnapshot_base: Schema.TaggedClass<TaskSnapshot, "TaskSnapshot"
50
49
  status: Schema.Literal<["running", "done", "failed"]>;
51
50
  countDisplay: Schema.Literal<["processedOnly", "detailed"]>;
52
51
  transient: typeof Schema.Boolean;
53
- units: Schema.Union<[typeof DeterminateTaskUnits, typeof IndeterminateTaskUnits]>;
52
+ units: Schema.Struct<{
53
+ succeeded: typeof Schema.Number;
54
+ failed: typeof Schema.Number;
55
+ processed: typeof Schema.Number;
56
+ total: Schema.optional<typeof Schema.Number>;
57
+ }>;
54
58
  startedAt: typeof Schema.Number;
55
59
  completedAt: Schema.NullOr<typeof Schema.Number>;
56
60
  }>;
@@ -64,22 +68,22 @@ interface TaskStore {
64
68
  readonly renderOrder: ReadonlyArray<RenderRow>;
65
69
  }
66
70
  interface ProgressService {
67
- readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
68
- readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
69
- readonly advanceTask: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
70
- readonly advanceTaskFailed: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
71
+ readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId, InvalidTaskTotalError>;
72
+ readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void, InvalidTaskTotalError>;
73
+ readonly incrementSucceeded: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
74
+ readonly incrementFailed: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
71
75
  readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
72
76
  readonly failTask: (taskId: TaskId) => Effect.Effect<void>;
73
77
  readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
74
78
  readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
75
79
  readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
76
80
  readonly runTask: {
77
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
78
- <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
81
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
82
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
79
83
  };
80
84
  readonly withTask: {
81
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
82
- <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
85
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
86
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
83
87
  };
84
88
  }
85
89
  declare const Task_base: Context.TagClass<Task, "stromseng.dev/effective-progress/Task", number & Brand.Brand<"TaskId">>;
@@ -155,15 +159,15 @@ interface EffectAllExecutionOptions extends EffectExecutionOptions {
155
159
  readonly mode?: "default" | "validate" | "either";
156
160
  }
157
161
  type AllOptions = Omit<TrackOptions, "total" | "countDisplay"> & EffectAllExecutionOptions;
158
- type AllReturn<Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions> = [[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>] ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : [Arg] extends [Record<string, Effect.Effect<any, any, any>>] ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : never] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress | Task>> : never;
162
+ type AllReturn<Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions> = [[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>] ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : [Arg] extends [Record<string, Effect.Effect<any, any, any>>] ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : never] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>> : never;
159
163
  interface ForEachExecutionOptions extends EffectExecutionOptions {
160
164
  readonly discard?: false | undefined;
161
165
  }
162
166
  type ForEachOptions = Omit<TrackOptions, "countDisplay"> & ForEachExecutionOptions;
163
167
  type TaskOptions = AddTaskOptions;
164
168
  declare const task: {
165
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
166
- <A, E, R>(options: TaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
169
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
170
+ <A, E, R>(options: TaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
167
171
  };
168
172
  type AllArg = ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>;
169
173
  declare const all: {
@@ -171,8 +175,8 @@ declare const all: {
171
175
  <O extends EffectAllExecutionOptions>(options: Omit<TrackOptions, "total" | "countDisplay"> & O): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
172
176
  };
173
177
  declare const forEach: {
174
- <A, B, E, R>(iterable: Iterable<A>, f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
175
- <A, B, E, R>(f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
178
+ <A, B, E, R>(iterable: Iterable<A>, f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): Effect.Effect<ReadonlyArray<B>, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
179
+ <A, B, E, R>(f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
176
180
  };
177
181
  //#endregion
178
182
  //#region src/services/stdio.d.ts
@@ -185,4 +189,4 @@ declare class ProgressStdio extends ProgressStdio_base {
185
189
  static readonly Default: Layer.Layer<ProgressStdio, never, never>;
186
190
  }
187
191
  //#endregion
188
- export { AddTaskOptions, AllOptions, AllReturn, DeterminateTaskUnits, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, IndeterminateTaskUnits, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
192
+ export { AddTaskOptions, AllOptions, AllReturn, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, InvalidTaskTotalError, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@ import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Sc
2
2
  import { dual } from "effect/Function";
3
3
  import { Box, Text, render } from "ink";
4
4
  import { useEffect, useState, useSyncExternalStore } from "react";
5
- import stringWidth from "string-width";
5
+ import stringWidth from "fast-string-width";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
 
8
8
  //#region src/types.ts
@@ -10,14 +10,16 @@ const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
10
10
  const TaskId = Brand.nominal();
11
11
  const TaskStatusSchema = Schema.Literal("running", "done", "failed");
12
12
  const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
13
- var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
13
+ var InvalidTaskTotalError = class extends Schema.TaggedError()("InvalidTaskTotalError", {
14
+ total: Schema.Number,
15
+ message: Schema.String
16
+ }) {};
17
+ const TaskUnitsSchema = Schema.Struct({
14
18
  succeeded: Schema.Number,
15
19
  failed: Schema.Number,
16
20
  processed: Schema.Number,
17
- total: Schema.Number
18
- }) {};
19
- var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", {}) {};
20
- const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
21
+ total: Schema.optional(Schema.Number)
22
+ });
21
23
  var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
22
24
  id: TaskIdSchema,
23
25
  parentId: Schema.NullOr(TaskIdSchema),
@@ -114,51 +116,75 @@ const toRenderSnapshot = (store) => {
114
116
 
115
117
  //#endregion
116
118
  //#region src/ink-renderer/store.ts
117
- const normalizeDeterminateCounts = (counts) => {
118
- const total = Math.max(0, counts.total);
119
+ const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
120
+ const INVALID_TASK_TOTAL_MESSAGE = "Task total must be greater than 0 when provided.";
121
+ const normalizeTotal = (total) => {
122
+ if (total <= 0) return Effect.fail(new InvalidTaskTotalError({
123
+ total,
124
+ message: INVALID_TASK_TOTAL_MESSAGE
125
+ }));
126
+ return Effect.succeed(total);
127
+ };
128
+ const normalizeUnits = (counts) => {
129
+ const total = counts.total;
130
+ if (total === void 0) {
131
+ const succeeded = Math.max(0, counts.succeeded);
132
+ const failed = Math.max(0, counts.failed);
133
+ return Effect.succeed({
134
+ succeeded,
135
+ failed,
136
+ processed: succeeded + failed
137
+ });
138
+ }
139
+ return Effect.map(normalizeTotal(total), (normalizedTotal) => {
140
+ const failed = Math.min(normalizedTotal, Math.max(0, counts.failed));
141
+ const succeeded = Math.min(normalizedTotal - failed, Math.max(0, counts.succeeded));
142
+ return {
143
+ succeeded,
144
+ failed,
145
+ processed: succeeded + failed,
146
+ total: normalizedTotal
147
+ };
148
+ });
149
+ };
150
+ const normalizeUnitsUnsafe = (counts) => {
151
+ const total = counts.total;
152
+ if (total === void 0) {
153
+ const succeeded = Math.max(0, counts.succeeded);
154
+ const failed = Math.max(0, counts.failed);
155
+ return {
156
+ succeeded,
157
+ failed,
158
+ processed: succeeded + failed
159
+ };
160
+ }
119
161
  const failed = Math.min(total, Math.max(0, counts.failed));
120
162
  const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
121
- return new DeterminateTaskUnits({
163
+ return {
122
164
  succeeded,
123
165
  failed,
124
166
  processed: succeeded + failed,
125
167
  total
126
- });
168
+ };
127
169
  };
128
- const updateDeterminateCounts = (units, options) => normalizeDeterminateCounts({
129
- succeeded: options.succeeded ?? units.succeeded,
130
- failed: options.failed ?? units.failed,
131
- total: options.total ?? units.total
132
- });
133
170
  const updatedSnapshot = (snapshot, options) => {
134
171
  const currentUnits = snapshot.units;
135
- const units = (() => {
136
- if (options.total !== void 0) {
137
- if (options.total <= 0) return new IndeterminateTaskUnits({});
138
- if (currentUnits._tag === "DeterminateTaskUnits") return updateDeterminateCounts(currentUnits, options);
139
- return normalizeDeterminateCounts({
140
- succeeded: options.succeeded ?? 0,
141
- failed: options.failed ?? 0,
142
- total: options.total
143
- });
144
- }
145
- if (currentUnits._tag === "DeterminateTaskUnits") {
146
- if (options.succeeded === void 0 && options.failed === void 0) return currentUnits;
147
- return updateDeterminateCounts(currentUnits, options);
148
- }
149
- return currentUnits;
150
- })();
151
- return new TaskSnapshot({
172
+ const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? Effect.succeed(currentUnits) : normalizeUnits({
173
+ succeeded: options.succeeded ?? currentUnits.succeeded,
174
+ failed: options.failed ?? currentUnits.failed,
175
+ total: hasExplicitTotal(options) ? options.total : currentUnits.total
176
+ });
177
+ return Effect.map(units, (resolvedUnits) => new TaskSnapshot({
152
178
  id: snapshot.id,
153
179
  parentId: snapshot.parentId,
154
180
  description: options.description ?? snapshot.description,
155
181
  status: snapshot.status,
156
182
  countDisplay: options.countDisplay ?? snapshot.countDisplay,
157
183
  transient: options.transient ?? snapshot.transient,
158
- units,
184
+ units: resolvedUnits,
159
185
  startedAt: snapshot.startedAt,
160
186
  completedAt: snapshot.completedAt
161
- });
187
+ }));
162
188
  };
163
189
  const withTransient = (snapshot, transient) => new TaskSnapshot({
164
190
  id: snapshot.id,
@@ -265,7 +291,7 @@ const makeProgressRenderStore = () => {
265
291
  },
266
292
  addTask: (options) => Effect.gen(function* () {
267
293
  const taskId = TaskId(++nextTaskId);
268
- const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({}) : normalizeDeterminateCounts({
294
+ const units = yield* normalizeUnits({
269
295
  succeeded: 0,
270
296
  failed: 0,
271
297
  total: options.total
@@ -301,12 +327,13 @@ const makeProgressRenderStore = () => {
301
327
  });
302
328
  return taskId;
303
329
  }),
304
- updateTask: (taskId, options) => Effect.sync(() => {
330
+ updateTask: (taskId, options) => Effect.gen(function* () {
331
+ const currentTask = state.tasks.get(taskId);
332
+ if (!currentTask) return;
333
+ const nextTask = yield* updatedSnapshot(currentTask, options);
305
334
  updateState((current) => {
306
- const currentTask = current.tasks.get(taskId);
307
- if (!currentTask) return current;
335
+ if (!current.tasks.get(taskId)) return current;
308
336
  const nextTasks = new Map(current.tasks);
309
- const nextTask = updatedSnapshot(currentTask, options);
310
337
  nextTasks.set(taskId, nextTask);
311
338
  if (options.transient !== void 0) for (const [candidateId, candidate] of current.tasks.entries()) {
312
339
  if (candidateId === taskId) continue;
@@ -327,10 +354,10 @@ const makeProgressRenderStore = () => {
327
354
  };
328
355
  });
329
356
  }),
330
- addSuccess: (taskId, amount = 1) => Effect.sync(() => {
357
+ incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
331
358
  updateState((current) => {
332
359
  const currentTask = current.tasks.get(taskId);
333
- if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
360
+ if (!currentTask) return current;
334
361
  const nextTasks = new Map(current.tasks);
335
362
  nextTasks.set(taskId, new TaskSnapshot({
336
363
  id: currentTask.id,
@@ -339,7 +366,7 @@ const makeProgressRenderStore = () => {
339
366
  status: currentTask.status,
340
367
  countDisplay: currentTask.countDisplay,
341
368
  transient: currentTask.transient,
342
- units: normalizeDeterminateCounts({
369
+ units: normalizeUnitsUnsafe({
343
370
  succeeded: currentTask.units.succeeded + amount,
344
371
  failed: currentTask.units.failed,
345
372
  total: currentTask.units.total
@@ -353,10 +380,10 @@ const makeProgressRenderStore = () => {
353
380
  };
354
381
  });
355
382
  }),
356
- addFailure: (taskId, amount = 1) => Effect.sync(() => {
383
+ incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
357
384
  updateState((current) => {
358
385
  const currentTask = current.tasks.get(taskId);
359
- if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
386
+ if (!currentTask) return current;
360
387
  const nextTasks = new Map(current.tasks);
361
388
  nextTasks.set(taskId, new TaskSnapshot({
362
389
  id: currentTask.id,
@@ -365,7 +392,7 @@ const makeProgressRenderStore = () => {
365
392
  status: currentTask.status,
366
393
  countDisplay: currentTask.countDisplay,
367
394
  transient: currentTask.transient,
368
- units: normalizeDeterminateCounts({
395
+ units: normalizeUnitsUnsafe({
369
396
  succeeded: currentTask.units.succeeded,
370
397
  failed: currentTask.units.failed + amount,
371
398
  total: currentTask.units.total
@@ -399,10 +426,14 @@ const makeProgressRenderStore = () => {
399
426
  status: "done",
400
427
  countDisplay: currentTask.countDisplay,
401
428
  transient: currentTask.transient,
402
- units: currentTask.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
429
+ units: currentTask.units.total !== void 0 ? normalizeUnitsUnsafe({
403
430
  succeeded: currentTask.units.total - currentTask.units.failed,
404
431
  failed: currentTask.units.failed,
405
432
  total: currentTask.units.total
433
+ }) : currentTask.units.processed > 0 ? normalizeUnitsUnsafe({
434
+ succeeded: currentTask.units.succeeded,
435
+ failed: currentTask.units.failed,
436
+ total: currentTask.units.processed
406
437
  }) : currentTask.units,
407
438
  startedAt: currentTask.startedAt,
408
439
  completedAt: now
@@ -462,6 +493,8 @@ const SPINNER_FRAMES = [
462
493
  "⠇",
463
494
  "⠏"
464
495
  ];
496
+ const isDeterminate$1 = (task) => task.units.total !== void 0;
497
+ const showsUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
465
498
  const formatDurationSeconds = (seconds) => {
466
499
  const value = Math.max(0, Math.floor(seconds));
467
500
  if (value < 60) return `${value}s`;
@@ -478,7 +511,7 @@ const formatElapsed = (task, now) => {
478
511
  return formatDurationSeconds(Math.max(0, (task.completedAt ?? now) - task.startedAt) / 1e3);
479
512
  };
480
513
  const formatEta = (task, now) => {
481
- if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") return "";
514
+ if (task.status !== "running" || !isDeterminate$1(task)) return "";
482
515
  const { processed, total } = task.units;
483
516
  const remaining = total - processed;
484
517
  if (processed <= 0 || remaining <= 0) return "";
@@ -494,7 +527,7 @@ const getTaskIndicator = (task, tick) => {
494
527
  symbol: "✗",
495
528
  color: "red"
496
529
  };
497
- if (task.units._tag !== "DeterminateTaskUnits") return {
530
+ if (!isDeterminate$1(task)) return {
498
531
  symbol: "✓",
499
532
  color: "green"
500
533
  };
@@ -517,7 +550,7 @@ const getTaskIndicator = (task, tick) => {
517
550
  };
518
551
  };
519
552
  const formatDeterminateAmountParts = (task) => {
520
- if (task.units._tag !== "DeterminateTaskUnits") return;
553
+ if (!isDeterminate$1(task)) return;
521
554
  const totalText = `${task.units.total}`;
522
555
  const width = totalText.length;
523
556
  const processedText = `${task.units.processed}`;
@@ -529,19 +562,23 @@ const formatDeterminateAmountParts = (task) => {
529
562
  };
530
563
  };
531
564
  const formatAmount = (task, _tick) => {
532
- if (task.units._tag === "DeterminateTaskUnits") {
565
+ if (isDeterminate$1(task)) {
533
566
  const parts = formatDeterminateAmountParts(task);
534
567
  if (parts === void 0) return "";
535
568
  if (task.countDisplay === "detailed") return `${parts.succeeded} ${parts.failed} ${parts.processed}/${parts.total}`;
536
569
  return `${parts.processed}/${parts.total}`;
537
570
  }
538
- if (task.status === "running" && task.units._tag === "IndeterminateTaskUnits") return "";
571
+ if (showsUnknownTotalCounts(task)) {
572
+ if (task.countDisplay === "detailed") return `${task.units.succeeded} ${task.units.failed} ${task.units.processed}/?`;
573
+ return `${task.units.processed}/?`;
574
+ }
575
+ if (task.status === "running") return "";
539
576
  return task.status === "failed" ? "✗" : "";
540
577
  };
541
578
 
542
579
  //#endregion
543
580
  //#region src/ink-renderer/columns/determinate.ts
544
- const isDeterminate = (task) => task.units._tag === "DeterminateTaskUnits";
581
+ const isDeterminate = (task) => task.units.total !== void 0;
545
582
  const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
546
583
 
547
584
  //#endregion
@@ -578,10 +615,11 @@ const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.v
578
615
  const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
579
616
  const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
580
617
  const blank = (width) => " ".repeat(Math.max(0, width));
581
- const shouldShowDetailedCounts = (task) => task.units._tag === "DeterminateTaskUnits" && task.countDisplay === "detailed";
618
+ const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
619
+ const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
582
620
  const SucceededCountColumn = ({ task, width }) => {
583
621
  if (width <= 0) return null;
584
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
622
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
585
623
  if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
586
624
  return /* @__PURE__ */ jsx(Text, {
587
625
  color: "green",
@@ -590,7 +628,7 @@ const SucceededCountColumn = ({ task, width }) => {
590
628
  };
591
629
  const FailedCountColumn = ({ task, width }) => {
592
630
  if (width <= 0) return null;
593
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
631
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
594
632
  if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
595
633
  return /* @__PURE__ */ jsx(Text, {
596
634
  color: "red",
@@ -599,11 +637,11 @@ const FailedCountColumn = ({ task, width }) => {
599
637
  };
600
638
  const ProcessedCountColumn = ({ task, width }) => {
601
639
  if (width <= 0) return null;
602
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
640
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
603
641
  return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
604
642
  };
605
643
  const AmountSeparatorColumn = ({ task, tick }) => {
606
- if (task.units._tag === "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: "/" });
644
+ if (shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: "/" });
607
645
  const symbol = formatAmount(task, tick);
608
646
  if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
609
647
  color: "red",
@@ -617,8 +655,8 @@ const AmountSeparatorColumn = ({ task, tick }) => {
617
655
  };
618
656
  const TotalCountColumn = ({ task, width }) => {
619
657
  if (width <= 0) return null;
620
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
621
- return /* @__PURE__ */ jsx(Text, { children: padRight(`${task.units.total}`, width) });
658
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
659
+ return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
622
660
  };
623
661
  const AmountColumn = ({ task, tick, layout }) => {
624
662
  if (layout.kind === "text") {
@@ -682,44 +720,47 @@ const AmountColumn = ({ task, tick, layout }) => {
682
720
  });
683
721
  };
684
722
  const computeAmountMetrics = (rows, tick) => {
685
- let hasDeterminate = false;
723
+ let hasStructuredCounts = false;
686
724
  let hasDetailed = false;
687
- let totalDigits = 0;
725
+ let countDigits = 0;
726
+ let totalWidth = 0;
688
727
  let simpleTextWidth = 0;
689
728
  for (const row of rows) {
690
729
  const { task } = row;
691
- if (isDeterminate(task)) {
692
- hasDeterminate = true;
693
- totalDigits = Math.max(totalDigits, textWidth(`${task.units.total}`));
730
+ if (shouldShowCountAmount(task)) {
731
+ hasStructuredCounts = true;
732
+ countDigits = Math.max(countDigits, textWidth(`${task.units.succeeded}`), textWidth(`${task.units.failed}`), textWidth(`${task.units.processed}`));
733
+ totalWidth = Math.max(totalWidth, textWidth(isDeterminate(task) ? `${task.units.total}` : "?"));
694
734
  if (task.countDisplay === "detailed") hasDetailed = true;
695
735
  continue;
696
736
  }
697
737
  simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
698
738
  }
699
739
  return {
700
- hasDeterminate,
740
+ hasStructuredCounts,
701
741
  hasDetailed,
702
- totalDigits: Math.max(1, totalDigits),
742
+ countDigits: Math.max(1, countDigits),
743
+ totalWidth: Math.max(1, totalWidth),
703
744
  simpleTextWidth
704
745
  };
705
746
  };
706
747
  const detailedAmountLayout = (metrics) => ({
707
748
  kind: "detailed",
708
- succeededWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
709
- failedWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
710
- processedWidth: metrics.totalDigits,
711
- totalWidth: metrics.totalDigits
749
+ succeededWidth: metrics.hasDetailed ? metrics.countDigits : 0,
750
+ failedWidth: metrics.hasDetailed ? metrics.countDigits : 0,
751
+ processedWidth: metrics.countDigits,
752
+ totalWidth: metrics.totalWidth
712
753
  });
713
754
  const processedAmountLayout = (metrics) => ({
714
755
  kind: "processed",
715
- processedWidth: metrics.totalDigits,
716
- totalWidth: metrics.totalDigits
756
+ processedWidth: metrics.countDigits,
757
+ totalWidth: metrics.totalWidth
717
758
  });
718
- const detailedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits + (metrics.hasDetailed ? metrics.totalDigits + 1 + metrics.totalDigits + 1 : 0);
719
- const processedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits;
759
+ const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
760
+ const processedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth;
720
761
  const createAmountColumnSpec = (context) => {
721
762
  const metrics = computeAmountMetrics(context.rows, context.tick);
722
- if (!metrics.hasDeterminate && metrics.simpleTextWidth <= 0) return;
763
+ if (!metrics.hasStructuredCounts && metrics.simpleTextWidth <= 0) return;
723
764
  const detailedLayout = detailedAmountLayout(metrics);
724
765
  const processedLayout = processedAmountLayout(metrics);
725
766
  const detailedWidth = detailedAmountWidth(metrics);
@@ -728,7 +769,7 @@ const createAmountColumnSpec = (context) => {
728
769
  id: "amount",
729
770
  grow: 0,
730
771
  canHide: true,
731
- variants: metrics.hasDeterminate && metrics.hasDetailed ? [{
772
+ variants: metrics.hasStructuredCounts && metrics.hasDetailed ? [{
732
773
  id: "detailed",
733
774
  minWidth: detailedWidth,
734
775
  idealWidth: detailedWidth,
@@ -746,7 +787,7 @@ const createAmountColumnSpec = (context) => {
746
787
  tick: context.tick,
747
788
  layout: processedLayout
748
789
  })
749
- }] : metrics.hasDeterminate ? [{
790
+ }] : metrics.hasStructuredCounts ? [{
750
791
  id: "processed",
751
792
  minWidth: processedWidth,
752
793
  idealWidth: processedWidth,
@@ -789,7 +830,7 @@ const segmentLengths = (width, total, succeeded, failed) => {
789
830
  };
790
831
  };
791
832
  const BarColumn = ({ task, width }) => {
792
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
833
+ if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, {});
793
834
  const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
794
835
  return /* @__PURE__ */ jsxs(Text, {
795
836
  wrap: "truncate-end",
@@ -1466,8 +1507,8 @@ const makeProgressService = Effect.gen(function* () {
1466
1507
  });
1467
1508
  });
1468
1509
  const updateTask = store.updateTask;
1469
- const advanceTask = store.addSuccess;
1470
- const advanceTaskFailed = store.addFailure;
1510
+ const incrementSucceeded = store.incrementSucceeded;
1511
+ const incrementFailed = store.incrementFailed;
1471
1512
  const completeTask = store.completeTask;
1472
1513
  const failTask = store.failTask;
1473
1514
  const getTask = store.getTask;
@@ -1485,8 +1526,8 @@ const makeProgressService = Effect.gen(function* () {
1485
1526
  const service = {
1486
1527
  addTask,
1487
1528
  updateTask,
1488
- advanceTask,
1489
- advanceTaskFailed,
1529
+ incrementSucceeded,
1530
+ incrementFailed,
1490
1531
  completeTask,
1491
1532
  failTask,
1492
1533
  log,
@@ -1546,18 +1587,18 @@ const allCountDisplay = (mode) => isCollectAllMode(mode) ? "detailed" : "process
1546
1587
  const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* () {
1547
1588
  const exit = yield* Effect.exit(effect);
1548
1589
  if (Exit.isSuccess(exit)) {
1549
- yield* progress.advanceTask(taskId, 1);
1590
+ yield* progress.incrementSucceeded(taskId, 1);
1550
1591
  return exit.value;
1551
1592
  }
1552
1593
  if (Cause.isInterruptedOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
1553
- yield* progress.advanceTaskFailed(taskId, 1);
1594
+ yield* progress.incrementFailed(taskId, 1);
1554
1595
  return yield* Effect.failCause(exit.cause);
1555
1596
  });
1556
1597
  const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
1557
1598
  const taskOption = yield* progress.getTask(taskId);
1558
- if (Option.isNone(taskOption) || taskOption.value.units._tag !== "DeterminateTaskUnits") return false;
1599
+ if (Option.isNone(taskOption)) return false;
1559
1600
  const { processed, total } = taskOption.value.units;
1560
- return processed >= total;
1601
+ return total !== void 0 && processed >= total;
1561
1602
  });
1562
1603
  const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
1563
1604
  const progress = yield* Progress;
@@ -1610,4 +1651,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1610
1651
  })));
1611
1652
 
1612
1653
  //#endregion
1613
- export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1654
+ export { InvalidTaskTotalError, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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": {
@@ -27,9 +27,6 @@
27
27
  "default": "./dist/index.mjs"
28
28
  }
29
29
  },
30
- "engines": {
31
- "node": ">=20.12.0"
32
- },
33
30
  "publishConfig": {
34
31
  "access": "public"
35
32
  },
@@ -43,9 +40,9 @@
43
40
  "format:check": "oxfmt --check ."
44
41
  },
45
42
  "dependencies": {
43
+ "fast-string-width": "^3.0.2",
46
44
  "ink": "^6.8.0",
47
- "react": "^19.2.4",
48
- "string-width": "^8.2.0"
45
+ "react": "^19.2.4"
49
46
  },
50
47
  "devDependencies": {
51
48
  "@effect/language-service": "^0.73.1",
@@ -59,5 +56,8 @@
59
56
  },
60
57
  "peerDependencies": {
61
58
  "effect": "^3.19.17"
59
+ },
60
+ "engines": {
61
+ "node": ">=20.12.0"
62
62
  }
63
63
  }