effective-progress 0.1.0 → 0.1.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
@@ -1,11 +1,11 @@
1
1
  # effective-progress
2
2
 
3
- `effective-progress` is an Effect-first terminal progress library with:
3
+ `effective-progress` is an [Effect](https://effect.website/)-first terminal progress library with:
4
4
 
5
5
  - multiple progress bars
6
6
  - nested child progress bars
7
7
  - spinner support for indeterminate work
8
- - clean log rendering alongside progress output
8
+ - clean log rendering alongside progress output, allowing you to simply use Effects `Console.log` or `Effect.logInfo`.
9
9
 
10
10
  ## Install
11
11
 
@@ -26,7 +26,7 @@ const program = Progress.all(
26
26
  yield* Console.log(`Completed task ${i + 1}`);
27
27
  }),
28
28
  ),
29
- { description: "Running tasks in parallel", all: { concurrency: 2 } },
29
+ { description: "Running tasks in parallel", concurrency: 2 },
30
30
  );
31
31
 
32
32
  Effect.runPromise(program);
@@ -62,7 +62,7 @@ const program = Progress.all(
62
62
  ),
63
63
  ),
64
64
  ),
65
- { description: "Running tasks in parallel", all: { concurrency: 2 } },
65
+ { description: "Running tasks in parallel", concurrency: 2 },
66
66
  );
67
67
 
68
68
  Effect.runPromise(program);
@@ -80,6 +80,12 @@ Effect.runPromise(program);
80
80
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
81
81
  - `examples/advancedExample.ts` - full API usage with custom config and manual task control
82
82
 
83
+ ## Log retention
84
+
85
+ - `maxLogLines` controls in-memory log retention.
86
+ - `maxLogLines` omitted or set to `0` means no log history is kept in memory.
87
+ - `maxLogLines > 0` keeps only the latest `N` log lines in memory.
88
+
83
89
  ## Notes
84
90
 
85
91
  - This is a WIP library, so expect breaking changes. Feedback and contributions are very welcome!
package/package.json CHANGED
@@ -1,37 +1,42 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Effect-first terminal progress bars with nested multibar support",
5
+ "homepage": "https://github.com/stromseng/effective-progress#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/stromseng/effective-progress/issues"
8
+ },
5
9
  "license": "MIT",
6
10
  "repository": {
7
11
  "type": "git",
8
12
  "url": "git+https://github.com/stromseng/effective-progress.git"
9
13
  },
10
- "bugs": {
11
- "url": "https://github.com/stromseng/effective-progress/issues"
12
- },
13
- "homepage": "https://github.com/stromseng/effective-progress#readme",
14
- "type": "module",
15
- "module": "index.ts",
16
14
  "files": [
17
15
  "index.ts",
18
16
  "src",
19
17
  "README.md",
20
18
  "LICENSE"
21
19
  ],
20
+ "type": "module",
21
+ "module": "index.ts",
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
25
  "scripts": {
26
- "prepare": "effect-language-service patch"
26
+ "prepare": "effect-language-service patch",
27
+ "typecheck": "tsc --noEmit",
28
+ "lint": "oxlint .",
29
+ "format": "oxfmt --write .",
30
+ "format:check": "oxfmt --check ."
27
31
  },
28
32
  "dependencies": {
29
- "@effect/language-service": "^0.73.1",
30
33
  "chalk": "^5.6.2",
31
34
  "effect": "^3.19.16"
32
35
  },
33
36
  "devDependencies": {
37
+ "@effect/language-service": "^0.73.1",
34
38
  "@types/bun": "latest",
39
+ "oxfmt": "^0.32.0",
35
40
  "oxlint": "^1.47.0"
36
41
  },
37
42
  "peerDependencies": {
package/src/api.ts CHANGED
@@ -1,48 +1,36 @@
1
1
  import { Console, Effect, Option } from "effect";
2
+ import type { Concurrency } from "effect/Types";
2
3
  import { makeProgressConsole } from "./console";
3
4
  import { makeProgressService } from "./runtime";
4
5
  import { Progress } from "./types";
5
6
  import type { TrackOptions } from "./types";
7
+ import { inferTotal } from "./utils";
6
8
 
7
- export interface TrackEffectsOptions {
8
- readonly description: string;
9
- readonly total?: number;
10
- readonly transient?: boolean;
11
- readonly all?: {
12
- readonly concurrency?: number | "unbounded";
13
- readonly batching?: boolean | "inherit";
14
- readonly concurrentFinalizers?: boolean;
15
- };
9
+ export interface EffectExecutionOptions {
10
+ readonly concurrency?: Concurrency;
11
+ readonly batching?: boolean | "inherit";
12
+ readonly concurrentFinalizers?: boolean;
16
13
  }
17
14
 
18
- export interface ForEachOptions extends TrackOptions {
19
- readonly forEach?: {
20
- readonly concurrency?: number | "unbounded";
21
- readonly batching?: boolean | "inherit";
22
- readonly concurrentFinalizers?: boolean;
23
- };
15
+ export interface EffectAllExecutionOptions extends EffectExecutionOptions {
16
+ readonly discard?: boolean;
17
+ readonly mode?: "default" | "validate" | "either";
24
18
  }
25
19
 
26
- const inferTotal = (iterable: Iterable<unknown>): number | undefined => {
27
- if (Array.isArray(iterable)) {
28
- return iterable.length;
29
- }
20
+ export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
21
+ export type AllReturn<
22
+ Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
23
+ O extends EffectAllExecutionOptions,
24
+ > =
25
+ [Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends
26
+ [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress>>
27
+ : never;
30
28
 
31
- if (typeof iterable === "string") {
32
- return iterable.length;
33
- }
34
-
35
- const candidate = iterable as { length?: unknown; size?: unknown };
36
- if (typeof candidate.length === "number") {
37
- return candidate.length;
38
- }
39
-
40
- if (typeof candidate.size === "number") {
41
- return candidate.size;
42
- }
29
+ export interface ForEachExecutionOptions extends EffectExecutionOptions {
30
+ readonly discard?: false | undefined;
31
+ }
43
32
 
44
- return undefined;
45
- };
33
+ export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
46
34
 
47
35
  export const withProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
48
36
  Effect.gen(function* () {
@@ -65,32 +53,36 @@ export const withProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
65
53
  );
66
54
  });
67
55
 
68
- export const all = <A, E, R>(
69
- effects: ReadonlyArray<Effect.Effect<A, E, R>>,
70
- options: TrackEffectsOptions,
71
- ): Effect.Effect<ReadonlyArray<A>, E, Exclude<R, Progress>> =>
56
+ export const all = <
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> =>
72
63
  withProgressService(
73
64
  Effect.gen(function* () {
74
65
  const progress = yield* Progress;
75
66
  return yield* progress.withTask(
76
67
  {
77
68
  description: options.description,
78
- total: options.total ?? effects.length,
69
+ total: effects.length,
79
70
  transient: options.transient,
80
71
  },
81
72
  (taskId) =>
82
- Effect.forEach(
73
+ Effect.all(
83
74
  effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
84
- (effect) => effect,
85
75
  {
86
- concurrency: options.all?.concurrency,
87
- batching: options.all?.batching,
88
- concurrentFinalizers: options.all?.concurrentFinalizers,
76
+ concurrency: options.concurrency,
77
+ batching: options.batching,
78
+ discard: options.discard,
79
+ mode: options.mode,
80
+ concurrentFinalizers: options.concurrentFinalizers,
89
81
  },
90
82
  ),
91
83
  );
92
84
  }),
93
- );
85
+ ) as AllReturn<Arg, O>;
94
86
 
95
87
  export const forEach = <A, B, E, R>(
96
88
  iterable: Iterable<A>,
@@ -101,34 +93,23 @@ export const forEach = <A, B, E, R>(
101
93
  Effect.gen(function* () {
102
94
  const progress = yield* Progress;
103
95
 
104
- const items = Array.from(iterable);
105
- const total = options.total ?? inferTotal(iterable);
106
-
107
96
  return yield* progress.withTask(
108
97
  {
109
98
  description: options.description,
110
- total,
99
+ total: options.total ?? inferTotal(iterable),
111
100
  transient: options.transient,
112
101
  },
113
102
  (taskId) =>
114
103
  Effect.forEach(
115
- items,
104
+ iterable,
116
105
  (item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
117
106
  {
118
- concurrency: options.forEach?.concurrency,
119
- batching: options.forEach?.batching,
120
- concurrentFinalizers: options.forEach?.concurrentFinalizers,
107
+ concurrency: options.concurrency,
108
+ batching: options.batching,
109
+ discard: options.discard,
110
+ concurrentFinalizers: options.concurrentFinalizers,
121
111
  },
122
112
  ),
123
113
  );
124
114
  }),
125
115
  );
126
-
127
- export const trackProgress = all;
128
-
129
- export const track = <A, B, E, R>(
130
- iterable: Iterable<A>,
131
- options: TrackOptions,
132
- f: (item: A, index: number) => Effect.Effect<B, E, R>,
133
- ): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress>> =>
134
- forEach(iterable, f, options);
package/src/renderer.ts CHANGED
@@ -1,11 +1,7 @@
1
1
  import { Effect, Ref } from "effect";
2
2
  import chalk from "chalk";
3
3
  import type { ProgressBarConfigShape } from "./types";
4
- import {
5
- DeterminateTaskUnits,
6
- TaskId,
7
- TaskSnapshot,
8
- } from "./types";
4
+ import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
9
5
 
10
6
  const HIDE_CURSOR = "\x1b[?25l";
11
7
  const SHOW_CURSOR = "\x1b[?25h";
@@ -13,11 +9,6 @@ const CLEAR_LINE = "\x1b[2K";
13
9
  const MOVE_UP_ONE = "\x1b[1A";
14
10
  const RENDER_INTERVAL = "80 millis";
15
11
 
16
- export interface LogEntry {
17
- readonly id: number;
18
- readonly message: string;
19
- }
20
-
21
12
  const renderDeterminate = (units: DeterminateTaskUnits, config: ProgressBarConfigShape): string => {
22
13
  const safeTotal = units.total <= 0 ? 1 : units.total;
23
14
  const ratio = Math.min(1, Math.max(0, units.completed / safeTotal));
@@ -80,82 +71,105 @@ const orderTasksForRender = (
80
71
 
81
72
  export const runProgressServiceRenderer = (
82
73
  tasksRef: Ref.Ref<Map<TaskId, TaskSnapshot>>,
83
- logsRef: Ref.Ref<ReadonlyArray<LogEntry>>,
74
+ logsRef: Ref.Ref<ReadonlyArray<string>>,
75
+ pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
84
76
  dirtyRef: Ref.Ref<boolean>,
85
77
  config: ProgressBarConfigShape,
78
+ maxRetainedLogLines: number,
86
79
  ) => {
87
80
  const isTTY = config.isTTY;
81
+ const retainLogHistory = maxRetainedLogLines > 0;
88
82
  let previousLineCount = 0;
89
- let nonTTYLastLogId = 0;
83
+ let previousTaskLineCount = 0;
90
84
  let nonTTYTaskSignatureById = new Map<number, string>();
91
85
  let tick = 0;
92
86
  let teardownInput: (() => void) | undefined;
93
87
 
94
- return Effect.gen(function* () {
95
- const clearTTY = () => {
96
- let output = "\r" + CLEAR_LINE;
97
- for (let i = 1; i < previousLineCount; i++) {
98
- output += MOVE_UP_ONE + CLEAR_LINE;
99
- }
100
- process.stderr.write(output + "\r");
101
- previousLineCount = 0;
102
- };
88
+ const clearTTYLines = (lineCount: number) => {
89
+ if (lineCount <= 0) {
90
+ return;
91
+ }
103
92
 
104
- const renderFrame = (mode: "tick" | "final") =>
105
- Effect.gen(function* () {
106
- const logs = yield* Ref.get(logsRef);
107
- const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
108
- (task) => !(task.transient && task.status !== "running"),
109
- );
110
- const ordered = orderTasksForRender(snapshots);
111
- const frameTick = mode === "final" ? tick + 1 : tick;
112
- const taskLines = ordered.map(({ snapshot, depth }) => {
113
- const lineTick = isTTY ? frameTick : 0;
114
- return buildTaskLine(snapshot, depth, lineTick, config);
115
- });
116
- const logLines = logs.map((log) => log.message);
117
- const lines = [...logLines, ...taskLines];
93
+ let output = "\r" + CLEAR_LINE;
94
+ for (let i = 1; i < lineCount; i++) {
95
+ output += MOVE_UP_ONE + CLEAR_LINE;
96
+ }
97
+ process.stderr.write(output + "\r");
98
+ };
118
99
 
119
- if (isTTY) {
120
- clearTTY();
121
- if (lines.length > 0) {
122
- process.stderr.write(lines.join("\n"));
123
- previousLineCount = lines.length;
124
- }
125
- } else {
126
- const appendedLogs = logs.filter((log) => log.id > nonTTYLastLogId);
127
- if (appendedLogs.length > 0) {
128
- process.stderr.write(appendedLogs.map((log) => log.message).join("\n") + "\n");
129
- nonTTYLastLogId = appendedLogs[appendedLogs.length - 1]?.id ?? nonTTYLastLogId;
130
- }
100
+ const renderNonTTYTaskUpdates = (
101
+ ordered: ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }>,
102
+ taskLines: ReadonlyArray<string>,
103
+ ) => {
104
+ const nextTaskSignatureById = new Map<number, string>();
105
+ const changedTaskLines: Array<string> = [];
106
+ const nonTtyUpdateStep = Math.max(1, Math.floor(config.nonTtyUpdateStep));
107
+
108
+ for (let i = 0; i < ordered.length; i++) {
109
+ const taskId = ordered[i]!.snapshot.id as number;
110
+ const snapshot = ordered[i]!.snapshot;
111
+ const line = taskLines[i]!;
112
+ const signature =
113
+ snapshot.units._tag === "DeterminateTaskUnits"
114
+ ? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
115
+ : `${snapshot.status}:${snapshot.description}`;
116
+
117
+ nextTaskSignatureById.set(taskId, signature);
118
+ if (nonTTYTaskSignatureById.get(taskId) !== signature) {
119
+ changedTaskLines.push(line);
120
+ }
121
+ }
131
122
 
132
- const nextTaskSignatureById = new Map<number, string>();
133
- const changedTaskLines: Array<string> = [];
134
- const nonTtyUpdateStep = Math.max(1, Math.floor(config.nonTtyUpdateStep));
123
+ if (changedTaskLines.length > 0) {
124
+ process.stderr.write(changedTaskLines.join("\n") + "\n");
125
+ }
135
126
 
136
- for (let i = 0; i < ordered.length; i++) {
137
- const taskId = ordered[i]!.snapshot.id as number;
138
- const snapshot = ordered[i]!.snapshot;
139
- const line = taskLines[i]!;
140
- const signature =
141
- snapshot.units._tag === "DeterminateTaskUnits"
142
- ? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
143
- : `${snapshot.status}:${snapshot.description}`;
127
+ nonTTYTaskSignatureById = nextTaskSignatureById;
128
+ };
144
129
 
145
- nextTaskSignatureById.set(taskId, signature);
146
- if (nonTTYTaskSignatureById.get(taskId) !== signature) {
147
- changedTaskLines.push(line);
148
- }
149
- }
130
+ const renderFrame = (mode: "tick" | "final") =>
131
+ Effect.gen(function* () {
132
+ const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
133
+ const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
134
+ (task) => !(task.transient && task.status !== "running"),
135
+ );
136
+ const ordered = orderTasksForRender(snapshots);
137
+ const frameTick = mode === "final" ? tick + 1 : tick;
138
+ const taskLines = ordered.map(({ snapshot, depth }) => {
139
+ const lineTick = isTTY ? frameTick : 0;
140
+ return buildTaskLine(snapshot, depth, lineTick, config);
141
+ });
150
142
 
151
- if (changedTaskLines.length > 0) {
152
- process.stderr.write(changedTaskLines.join("\n") + "\n");
143
+ if (isTTY) {
144
+ if (retainLogHistory) {
145
+ const historyLogs = yield* Ref.get(logsRef);
146
+ const lines = [...historyLogs, ...taskLines];
147
+ clearTTYLines(previousLineCount);
148
+ if (lines.length > 0) {
149
+ process.stderr.write(lines.join("\n"));
153
150
  }
151
+ previousLineCount = lines.length;
152
+ return;
153
+ }
154
154
 
155
- nonTTYTaskSignatureById = nextTaskSignatureById;
155
+ clearTTYLines(previousTaskLineCount);
156
+ if (drainedLogs.length > 0) {
157
+ process.stderr.write(drainedLogs.join("\n") + "\n");
156
158
  }
157
- });
159
+ if (taskLines.length > 0) {
160
+ process.stderr.write(taskLines.join("\n"));
161
+ }
162
+ previousTaskLineCount = taskLines.length;
163
+ return;
164
+ }
158
165
 
166
+ if (drainedLogs.length > 0) {
167
+ process.stderr.write(drainedLogs.join("\n") + "\n");
168
+ }
169
+ renderNonTTYTaskUpdates(ordered, taskLines);
170
+ });
171
+
172
+ return Effect.gen(function* () {
159
173
  if (isTTY) {
160
174
  process.stderr.write(HIDE_CURSOR);
161
175
 
@@ -205,60 +219,7 @@ export const runProgressServiceRenderer = (
205
219
  }).pipe(
206
220
  Effect.ensuring(
207
221
  Effect.gen(function* () {
208
- const logs = yield* Ref.get(logsRef);
209
- const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
210
- (task) => !(task.transient && task.status !== "running"),
211
- );
212
- const ordered = orderTasksForRender(snapshots);
213
- const taskLines = ordered.map(({ snapshot, depth }) =>
214
- buildTaskLine(snapshot, depth, tick + 1, config),
215
- );
216
- const logLines = logs.map((log) => log.message);
217
- const lines = [...logLines, ...taskLines];
218
-
219
- if (isTTY) {
220
- let output = "\r" + CLEAR_LINE;
221
- for (let i = 1; i < previousLineCount; i++) {
222
- output += MOVE_UP_ONE + CLEAR_LINE;
223
- }
224
-
225
- if (lines.length > 0) {
226
- process.stderr.write(output + "\r" + lines.join("\n"));
227
- } else {
228
- process.stderr.write(output + "\r");
229
- }
230
- } else {
231
- const appendedLogs = logs.filter((log) => log.id > nonTTYLastLogId);
232
- if (appendedLogs.length > 0) {
233
- process.stderr.write(appendedLogs.map((log) => log.message).join("\n") + "\n");
234
- nonTTYLastLogId = appendedLogs[appendedLogs.length - 1]?.id ?? nonTTYLastLogId;
235
- }
236
-
237
- const nextTaskSignatureById = new Map<number, string>();
238
- const changedTaskLines: Array<string> = [];
239
- const nonTtyUpdateStep = Math.max(1, Math.floor(config.nonTtyUpdateStep));
240
-
241
- for (let i = 0; i < ordered.length; i++) {
242
- const taskId = ordered[i]!.snapshot.id as number;
243
- const snapshot = ordered[i]!.snapshot;
244
- const line = taskLines[i]!;
245
- const signature =
246
- snapshot.units._tag === "DeterminateTaskUnits"
247
- ? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
248
- : `${snapshot.status}:${snapshot.description}`;
249
-
250
- nextTaskSignatureById.set(taskId, signature);
251
- if (nonTTYTaskSignatureById.get(taskId) !== signature) {
252
- changedTaskLines.push(line);
253
- }
254
- }
255
-
256
- if (changedTaskLines.length > 0) {
257
- process.stderr.write(changedTaskLines.join("\n") + "\n");
258
- }
259
-
260
- nonTTYTaskSignatureById = nextTaskSignatureById;
261
- }
222
+ yield* renderFrame("final");
262
223
 
263
224
  if (isTTY) {
264
225
  teardownInput?.();
package/src/runtime.ts CHANGED
@@ -1,44 +1,20 @@
1
1
  import { Effect, Exit, FiberRef, Option, Ref } from "effect";
2
2
  import { formatWithOptions } from "node:util";
3
3
  import { runProgressServiceRenderer } from "./renderer";
4
- import type {
5
- AddTaskOptions,
6
- ProgressService,
7
- TrackOptions,
8
- UpdateTaskOptions,
9
- } from "./types";
4
+ import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
10
5
  import {
11
6
  defaultProgressBarConfig,
12
7
  DeterminateTaskUnits,
13
8
  IndeterminateTaskUnits,
9
+ Progress,
14
10
  ProgressBarConfig,
15
11
  TaskId,
16
12
  TaskSnapshot,
17
13
  } from "./types";
14
+ import { inferTotal } from "./utils";
18
15
 
19
16
  const DIRTY_DEBOUNCE_INTERVAL = "10 millis";
20
17
 
21
- const inferTotal = (iterable: Iterable<unknown>): number | undefined => {
22
- if (Array.isArray(iterable)) {
23
- return iterable.length;
24
- }
25
-
26
- if (typeof iterable === "string") {
27
- return iterable.length;
28
- }
29
-
30
- const candidate = iterable as { length?: unknown; size?: unknown };
31
- if (typeof candidate.length === "number") {
32
- return candidate.length;
33
- }
34
-
35
- if (typeof candidate.size === "number") {
36
- return candidate.size;
37
- }
38
-
39
- return undefined;
40
- };
41
-
42
18
  const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
43
19
  const currentUnits = snapshot.units;
44
20
  const units = (() => {
@@ -84,16 +60,26 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
84
60
  export const makeProgressService = Effect.gen(function* () {
85
61
  const configOption = yield* Effect.serviceOption(ProgressBarConfig);
86
62
  const config = Option.getOrElse(configOption, () => defaultProgressBarConfig);
63
+ const maxRetainedLogLines = Math.max(0, Math.floor(config.maxLogLines ?? 0));
87
64
 
88
65
  const nextTaskIdRef = yield* Ref.make(0);
89
66
  const tasksRef = yield* Ref.make(new Map<TaskId, TaskSnapshot>());
90
- const logsRef = yield* Ref.make<ReadonlyArray<{ id: number; message: string }>>([]);
67
+ const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
68
+ const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
91
69
  const dirtyRef = yield* Ref.make(true);
92
70
  const dirtyScheduledRef = yield* Ref.make(false);
93
- const nextLogIdRef = yield* Ref.make(0);
94
71
  const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
95
72
 
96
- yield* Effect.forkScoped(runProgressServiceRenderer(tasksRef, logsRef, dirtyRef, config));
73
+ yield* Effect.forkScoped(
74
+ runProgressServiceRenderer(
75
+ tasksRef,
76
+ logsRef,
77
+ pendingLogsRef,
78
+ dirtyRef,
79
+ config,
80
+ maxRetainedLogLines,
81
+ ),
82
+ );
97
83
 
98
84
  const markDirty = Effect.gen(function* () {
99
85
  const shouldSchedule = yield* Ref.modify(dirtyScheduledRef, (scheduled) =>
@@ -251,7 +237,10 @@ export const makeProgressService = Effect.gen(function* () {
251
237
 
252
238
  const log = (...args: ReadonlyArray<unknown>) =>
253
239
  Effect.gen(function* () {
254
- const id = yield* Ref.updateAndGet(nextLogIdRef, (current) => current + 1);
240
+ if (args.length === 0) {
241
+ return;
242
+ }
243
+
255
244
  const message = formatWithOptions(
256
245
  {
257
246
  colors: config.isTTY,
@@ -260,17 +249,17 @@ export const makeProgressService = Effect.gen(function* () {
260
249
  ...args,
261
250
  );
262
251
 
263
- yield* Ref.update(logsRef, (logs) => {
264
- const maxLogLines = Math.floor(config.maxLogLines);
265
- const next = [...logs, { id, message }];
266
- if (maxLogLines <= 0) {
267
- return next;
268
- }
269
- if (next.length <= maxLogLines) {
270
- return next;
271
- }
272
- return next.slice(next.length - maxLogLines);
273
- });
252
+ yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
253
+ if (maxRetainedLogLines > 0) {
254
+ yield* Ref.update(logsRef, (logs) => {
255
+ const next = [...logs, message];
256
+ if (next.length <= maxRetainedLogLines) {
257
+ return next;
258
+ }
259
+ return next.slice(next.length - maxRetainedLogLines);
260
+ });
261
+ }
262
+
274
263
  yield* markDirty;
275
264
  });
276
265
 
@@ -315,14 +304,13 @@ export const makeProgressService = Effect.gen(function* () {
315
304
  transient: options.transient,
316
305
  },
317
306
  (taskId) => {
318
- const items = Array.from(iterable);
319
- return Effect.forEach(items, (item, index) =>
307
+ return Effect.forEach(iterable, (item, index) =>
320
308
  Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
321
309
  );
322
310
  },
323
311
  );
324
312
 
325
- const service: ProgressService = {
313
+ return Progress.of({
326
314
  addTask,
327
315
  updateTask,
328
316
  advanceTask,
@@ -333,7 +321,5 @@ export const makeProgressService = Effect.gen(function* () {
333
321
  listTasks,
334
322
  withTask,
335
323
  trackIterable,
336
- };
337
-
338
- return service;
324
+ });
339
325
  });
package/src/types.ts CHANGED
@@ -12,7 +12,7 @@ export const ProgressBarConfigSchema = Schema.Struct({
12
12
  emptyChar: Schema.String,
13
13
  leftBracket: Schema.String,
14
14
  rightBracket: Schema.String,
15
- maxLogLines: Schema.Number,
15
+ maxLogLines: Schema.optional(Schema.Number),
16
16
  nonTtyUpdateStep: Schema.Number,
17
17
  });
18
18
 
@@ -59,11 +59,7 @@ export interface UpdateTaskOptions {
59
59
  readonly transient?: boolean;
60
60
  }
61
61
 
62
- export interface TrackOptions {
63
- readonly description: string;
64
- readonly total?: number;
65
- readonly transient?: boolean;
66
- }
62
+ export type TrackOptions = Exclude<AddTaskOptions, "parentId">;
67
63
 
68
64
  export class DeterminateTaskUnits extends Schema.TaggedClass<DeterminateTaskUnits>()(
69
65
  "DeterminateTaskUnits",
@@ -160,8 +156,3 @@ export const ProgressTaskEventSchema = Schema.Union(
160
156
  export type ProgressTaskEvent = typeof ProgressTaskEventSchema.Type;
161
157
 
162
158
  export const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
163
-
164
- export const isIndeterminateTask = (snapshot: TaskSnapshot) =>
165
- snapshot.units._tag === "IndeterminateTaskUnits";
166
-
167
- export const nextSpinnerFrame = (index: number) => (index + 1) % SPINNER_FRAMES.length;
package/src/utils.ts ADDED
@@ -0,0 +1,20 @@
1
+ export const inferTotal = (iterable: Iterable<unknown>): number | undefined => {
2
+ if (Array.isArray(iterable)) {
3
+ return iterable.length;
4
+ }
5
+
6
+ if (typeof iterable === "string") {
7
+ return iterable.length;
8
+ }
9
+
10
+ const candidate = iterable as { length?: unknown; size?: unknown };
11
+ if (typeof candidate.length === "number") {
12
+ return candidate.length;
13
+ }
14
+
15
+ if (typeof candidate.size === "number") {
16
+ return candidate.size;
17
+ }
18
+
19
+ return undefined;
20
+ };