effective-progress 0.6.2 → 0.8.0

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
@@ -83,12 +83,14 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
83
83
  - In fail-fast runs, unresolved units remain unprocessed.
84
84
  - `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
85
85
  - Mixed outcomes can still finalize as `done` when all units are accounted for.
86
+ - Empty collections are valid inputs for `Progress.all` / `Progress.forEach` and render as `0/0` instead of failing.
86
87
 
87
88
  ### Other examples
88
89
 
89
90
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
90
91
  - `examples/advancedExample.ts` - full API usage and manual task control
91
92
  - `examples/mixedOutcomes.ts` - fail-fast vs `either`/`validate` with mixed success/failure counters
93
+ - `examples/cliProgressSemantics.ts` - zero totals, negative totals clearing to unknown totals, overflow counts, and empty `all` / `forEach`
92
94
  - `examples/unknownTotalCounting.ts` - count successes/failures without a known total and render `processed/?`
93
95
  - `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
94
96
  - `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
@@ -107,6 +109,8 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
107
109
  - Built-in columns are: description, bar, amount/spinner, elapsed, and ETA.
108
110
  - Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
109
111
  - Determinate amount text shows counters without prefixes: `<succeeded> <failed> <processed>/<total>`.
112
+ - Counts can exceed `total`; the amount text keeps those raw values (for example `12/10`) while the bar stays visually clamped at full.
113
+ - `total: 0` is valid for determinate tasks and renders as a full bar by default.
110
114
  - Column widths are shared per frame (widest visible cell wins), so rows stay aligned.
111
115
  - Elapsed and ETA reserve stable widths to reduce jitter while tasks transition states.
112
116
  - Layout uses a 100-column baseline and grows when content requires more space.
@@ -132,6 +136,12 @@ const program = Progress.task(
132
136
  );
133
137
  ```
134
138
 
139
+ Manual total behavior:
140
+
141
+ - negative totals on task creation clear the total and switch to indeterminate rendering
142
+ - negative totals on later `updateTask` calls also clear the total
143
+ - explicit `total: undefined` on `updateTask` clears the total and switches back to indeterminate rendering
144
+
135
145
  ## Column customization
136
146
 
137
147
  Custom column APIs are not part of the first Ink release. The renderer ships with built-in columns only, and old renderer config APIs (`RendererConfig`, `ProgressBarConfig`, custom column definitions) are intentionally removed in this iteration.
package/dist/index.d.mts CHANGED
@@ -10,13 +10,6 @@ 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 {}
20
13
  interface AddTaskOptions {
21
14
  readonly description: string;
22
15
  readonly total?: number;
@@ -68,8 +61,8 @@ interface TaskStore {
68
61
  readonly renderOrder: ReadonlyArray<RenderRow>;
69
62
  }
70
63
  interface ProgressService {
71
- readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId, InvalidTaskTotalError>;
72
- readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void, InvalidTaskTotalError>;
64
+ readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
65
+ readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
73
66
  readonly incrementSucceeded: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
74
67
  readonly incrementFailed: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
75
68
  readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
@@ -78,12 +71,12 @@ interface ProgressService {
78
71
  readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
79
72
  readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
80
73
  readonly runTask: {
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>>;
74
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
75
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
83
76
  };
84
77
  readonly withTask: {
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>>;
78
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
79
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
87
80
  };
88
81
  }
89
82
  declare const Task_base: Context.TagClass<Task, "stromseng.dev/effective-progress/Task", number & Brand.Brand<"TaskId">>;
@@ -159,15 +152,15 @@ interface EffectAllExecutionOptions extends EffectExecutionOptions {
159
152
  readonly mode?: "default" | "validate" | "either";
160
153
  }
161
154
  type AllOptions = Omit<TrackOptions, "total" | "countDisplay"> & EffectAllExecutionOptions;
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;
155
+ 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;
163
156
  interface ForEachExecutionOptions extends EffectExecutionOptions {
164
157
  readonly discard?: false | undefined;
165
158
  }
166
159
  type ForEachOptions = Omit<TrackOptions, "countDisplay"> & ForEachExecutionOptions;
167
160
  type TaskOptions = AddTaskOptions;
168
161
  declare const 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>>;
162
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
163
+ <A, E, R>(options: TaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
171
164
  };
172
165
  type AllArg = ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>;
173
166
  declare const all: {
@@ -175,8 +168,8 @@ declare const all: {
175
168
  <O extends EffectAllExecutionOptions>(options: Omit<TrackOptions, "total" | "countDisplay"> & O): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
176
169
  };
177
170
  declare const forEach: {
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>>;
171
+ <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>>;
172
+ <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>>;
180
173
  };
181
174
  //#endregion
182
175
  //#region src/services/stdio.d.ts
@@ -189,4 +182,4 @@ declare class ProgressStdio extends ProgressStdio_base {
189
182
  static readonly Default: Layer.Layer<ProgressStdio, never, never>;
190
183
  }
191
184
  //#endregion
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 };
185
+ export { AddTaskOptions, AllOptions, AllReturn, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, 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
@@ -10,10 +10,6 @@ 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 InvalidTaskTotalError = class extends Schema.TaggedError()("InvalidTaskTotalError", {
14
- total: Schema.Number,
15
- message: Schema.String
16
- }) {};
17
13
  const TaskUnitsSchema = Schema.Struct({
18
14
  succeeded: Schema.Number,
19
15
  failed: Schema.Number,
@@ -117,74 +113,46 @@ const toRenderSnapshot = (store) => {
117
113
  //#endregion
118
114
  //#region src/ink-renderer/store.ts
119
115
  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);
116
+ const sanitizeTotalOnAdd = (total) => {
117
+ if (total === void 0) return;
118
+ return total < 0 ? void 0 : total;
127
119
  };
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
- });
120
+ const sanitizeTotalOnUpdate = (nextTotal) => {
121
+ if (nextTotal === void 0) return;
122
+ return nextTotal < 0 ? void 0 : nextTotal;
149
123
  };
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
- }
161
- const failed = Math.min(total, Math.max(0, counts.failed));
162
- const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
163
- return {
124
+ const normalizeUnits = (counts) => {
125
+ const succeeded = Math.max(0, counts.succeeded);
126
+ const failed = Math.max(0, counts.failed);
127
+ return counts.total === void 0 ? {
128
+ succeeded,
129
+ failed,
130
+ processed: succeeded + failed
131
+ } : {
164
132
  succeeded,
165
133
  failed,
166
134
  processed: succeeded + failed,
167
- total
135
+ total: counts.total
168
136
  };
169
137
  };
170
138
  const updatedSnapshot = (snapshot, options) => {
171
139
  const currentUnits = snapshot.units;
172
- const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? Effect.succeed(currentUnits) : normalizeUnits({
140
+ const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
173
141
  succeeded: options.succeeded ?? currentUnits.succeeded,
174
142
  failed: options.failed ?? currentUnits.failed,
175
- total: hasExplicitTotal(options) ? options.total : currentUnits.total
143
+ total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
176
144
  });
177
- return Effect.map(units, (resolvedUnits) => new TaskSnapshot({
145
+ return new TaskSnapshot({
178
146
  id: snapshot.id,
179
147
  parentId: snapshot.parentId,
180
148
  description: options.description ?? snapshot.description,
181
149
  status: snapshot.status,
182
150
  countDisplay: options.countDisplay ?? snapshot.countDisplay,
183
151
  transient: options.transient ?? snapshot.transient,
184
- units: resolvedUnits,
152
+ units,
185
153
  startedAt: snapshot.startedAt,
186
154
  completedAt: snapshot.completedAt
187
- }));
155
+ });
188
156
  };
189
157
  const withTransient = (snapshot, transient) => new TaskSnapshot({
190
158
  id: snapshot.id,
@@ -291,10 +259,10 @@ const makeProgressRenderStore = () => {
291
259
  },
292
260
  addTask: (options) => Effect.gen(function* () {
293
261
  const taskId = TaskId(++nextTaskId);
294
- const units = yield* normalizeUnits({
262
+ const units = normalizeUnits({
295
263
  succeeded: 0,
296
264
  failed: 0,
297
- total: options.total
265
+ total: sanitizeTotalOnAdd(options.total)
298
266
  });
299
267
  const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
300
268
  const now = yield* Clock.currentTimeMillis;
@@ -330,7 +298,7 @@ const makeProgressRenderStore = () => {
330
298
  updateTask: (taskId, options) => Effect.gen(function* () {
331
299
  const currentTask = state.tasks.get(taskId);
332
300
  if (!currentTask) return;
333
- const nextTask = yield* updatedSnapshot(currentTask, options);
301
+ const nextTask = updatedSnapshot(currentTask, options);
334
302
  updateState((current) => {
335
303
  if (!current.tasks.get(taskId)) return current;
336
304
  const nextTasks = new Map(current.tasks);
@@ -366,7 +334,7 @@ const makeProgressRenderStore = () => {
366
334
  status: currentTask.status,
367
335
  countDisplay: currentTask.countDisplay,
368
336
  transient: currentTask.transient,
369
- units: normalizeUnitsUnsafe({
337
+ units: normalizeUnits({
370
338
  succeeded: currentTask.units.succeeded + amount,
371
339
  failed: currentTask.units.failed,
372
340
  total: currentTask.units.total
@@ -392,7 +360,7 @@ const makeProgressRenderStore = () => {
392
360
  status: currentTask.status,
393
361
  countDisplay: currentTask.countDisplay,
394
362
  transient: currentTask.transient,
395
- units: normalizeUnitsUnsafe({
363
+ units: normalizeUnits({
396
364
  succeeded: currentTask.units.succeeded,
397
365
  failed: currentTask.units.failed + amount,
398
366
  total: currentTask.units.total
@@ -426,11 +394,11 @@ const makeProgressRenderStore = () => {
426
394
  status: "done",
427
395
  countDisplay: currentTask.countDisplay,
428
396
  transient: currentTask.transient,
429
- units: currentTask.units.total !== void 0 ? normalizeUnitsUnsafe({
430
- succeeded: currentTask.units.total - currentTask.units.failed,
397
+ units: currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
398
+ succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
431
399
  failed: currentTask.units.failed,
432
400
  total: currentTask.units.total
433
- }) : currentTask.units.processed > 0 ? normalizeUnitsUnsafe({
401
+ }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
434
402
  succeeded: currentTask.units.succeeded,
435
403
  failed: currentTask.units.failed,
436
404
  total: currentTask.units.processed
@@ -814,13 +782,14 @@ const createAmountColumnSpec = (context) => {
814
782
  const DEFAULT_BAR_WIDTH = 30;
815
783
  const MIN_BAR_WIDTH = 8;
816
784
  const segmentLengths = (width, total, succeeded, failed) => {
817
- if (total <= 0) return {
818
- succeeded: 0,
785
+ if (total === 0) return {
786
+ succeeded: width,
819
787
  failed: 0,
820
- remaining: width
788
+ remaining: 0
821
789
  };
822
- const succeededEnd = Math.round(succeeded / total * width);
823
- const failedEnd = Math.round((succeeded + failed) / total * width);
790
+ const displayTotal = Math.max(total, succeeded + failed);
791
+ const succeededEnd = Math.round(succeeded / displayTotal * width);
792
+ const failedEnd = Math.round((succeeded + failed) / displayTotal * width);
824
793
  const succeededLength = Math.max(0, Math.min(width, succeededEnd));
825
794
  const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
826
795
  return {
@@ -1651,4 +1620,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1651
1620
  })));
1652
1621
 
1653
1622
  //#endregion
1654
- export { InvalidTaskTotalError, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1623
+ export { 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.2",
3
+ "version": "0.8.0",
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": {