effective-progress 0.2.1 → 0.2.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
@@ -119,19 +119,16 @@ Task-level `progressbar` config is optional and inherits from its parent task (o
119
119
 
120
120
  ```ts
121
121
  yield *
122
- progress.withTask(
123
- Effect.sleep("1 second"),
124
- {
125
- description: "Worker pipeline",
126
- progressbar: {
127
- barWidth: 20,
128
- spinnerFrames: [".", "o", "O", "0"],
129
- colors: {
130
- spinner: { kind: "named", value: "magentaBright" },
131
- },
122
+ progress.withTask(Effect.sleep("1 second"), {
123
+ description: "Worker pipeline",
124
+ progressbar: {
125
+ barWidth: 20,
126
+ spinnerFrames: [".", "o", "O", "0"],
127
+ colors: {
128
+ spinner: { kind: "named", value: "magentaBright" },
132
129
  },
133
130
  },
134
- );
131
+ });
135
132
  ```
136
133
 
137
134
  ## Terminal service and mocking
@@ -158,17 +155,17 @@ const mockTerminal: Progress.ProgressTerminalService = {
158
155
  withRawInputCapture: (effect) => effect,
159
156
  };
160
157
 
161
- const program = Progress.withTask(Effect.sleep("100 millis"), { description: "work" }).pipe(
158
+ const program = Progress.task(Effect.sleep("100 millis"), { description: "work" }).pipe(
162
159
  Effect.provideService(Progress.ProgressTerminal, mockTerminal),
163
160
  );
164
161
  ```
165
162
 
166
163
  ## Manual task control
167
164
 
168
- For manual usage, `withTask` captures logs implicitly and provides the current `Task` context:
165
+ For manual usage, `task` captures logs implicitly and provides the current `Task` context:
169
166
 
170
167
  ```ts
171
- const program = Progress.withTask(
168
+ const program = Progress.task(
172
169
  Effect.gen(function* () {
173
170
  const currentTask = yield* Progress.Task;
174
171
  yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.2.1",
3
+ "version": "0.2.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": {
package/src/api.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Effect } from "effect";
1
+ import { Effect, Exit } from "effect";
2
2
  import { dual } from "effect/Function";
3
3
  import type { Concurrency } from "effect/Types";
4
4
  import { Progress, provideProgressService } from "./runtime";
@@ -21,9 +21,10 @@ export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions
21
21
  export type AllReturn<
22
22
  Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
23
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 | Task>>
24
+ > = [Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends [
25
+ Effect.Effect<infer A, infer E, infer R>,
26
+ ]
27
+ ? Effect.Effect<A, E, Exclude<R, Progress | Task>>
27
28
  : never;
28
29
 
29
30
  export interface ForEachExecutionOptions extends EffectExecutionOptions {
@@ -32,7 +33,7 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
32
33
 
33
34
  export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
34
35
 
35
- export const withTask: {
36
+ export const task: {
36
37
  <A, E, R>(
37
38
  effect: Effect.Effect<A, E, R>,
38
39
  options: AddTaskOptions,
@@ -40,48 +41,68 @@ export const withTask: {
40
41
  <A, E, R>(
41
42
  options: AddTaskOptions,
42
43
  ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
43
- } = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
44
- provideProgressService(
45
- Effect.gen(function* () {
46
- const progress = yield* Progress;
47
- return yield* progress.withTask(effect, options);
48
- }),
49
- ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
44
+ } = dual(
45
+ 2,
46
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
47
+ provideProgressService(
48
+ Effect.gen(function* () {
49
+ const progress = yield* Progress;
50
+ return yield* progress.withTask(effect, options);
51
+ }),
52
+ ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
50
53
  );
51
54
 
52
55
  export const all: {
53
- <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
56
+ <
57
+ const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
58
+ O extends EffectAllExecutionOptions,
59
+ >(
54
60
  effects: Arg,
55
61
  options: Omit<TrackOptions, "total"> & O,
56
62
  ): AllReturn<Arg, O>;
57
63
  <O extends EffectAllExecutionOptions>(
58
64
  options: Omit<TrackOptions, "total"> & O,
59
- ): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(effects: Arg) => AllReturn<
60
- Arg,
61
- O
62
- >;
65
+ ): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(
66
+ effects: Arg,
67
+ ) => AllReturn<Arg, O>;
63
68
  } = dual(
64
69
  2,
65
- <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
70
+ <
71
+ const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
72
+ O extends EffectAllExecutionOptions,
73
+ >(
66
74
  effects: Arg,
67
75
  options: Omit<TrackOptions, "total"> & O,
68
76
  ) =>
69
77
  provideProgressService(
70
78
  Effect.gen(function* () {
71
79
  const progress = yield* Progress;
72
- return yield* progress.withTask(
80
+ return yield* progress.runTask(
73
81
  Effect.gen(function* () {
74
82
  const taskId = yield* Task;
75
- return yield* Effect.all(
76
- effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
77
- {
78
- concurrency: options.concurrency,
79
- batching: options.batching,
80
- discard: options.discard,
81
- mode: options.mode,
82
- concurrentFinalizers: options.concurrentFinalizers,
83
- },
83
+ const exit = yield* Effect.exit(
84
+ Effect.all(
85
+ effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
86
+ {
87
+ concurrency: options.concurrency,
88
+ batching: options.batching,
89
+ discard: options.discard,
90
+ mode: options.mode,
91
+ concurrentFinalizers: options.concurrentFinalizers,
92
+ },
93
+ ),
84
94
  );
95
+
96
+ if (Exit.isSuccess(exit)) {
97
+ yield* progress.completeTask(taskId);
98
+ } else {
99
+ yield* progress.failTask(taskId);
100
+ }
101
+
102
+ return yield* Exit.match(exit, {
103
+ onFailure: Effect.failCause,
104
+ onSuccess: Effect.succeed,
105
+ });
85
106
  }),
86
107
  {
87
108
  description: options.description,
@@ -103,9 +124,7 @@ export const forEach: {
103
124
  <A, B, E, R>(
104
125
  f: (item: A, index: number) => Effect.Effect<B, E, R>,
105
126
  options: ForEachOptions,
106
- ): (
107
- iterable: Iterable<A>,
108
- ) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
127
+ ): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
109
128
  } = dual(
110
129
  3,
111
130
  <A, B, E, R>(
@@ -117,20 +136,32 @@ export const forEach: {
117
136
  Effect.gen(function* () {
118
137
  const progress = yield* Progress;
119
138
 
120
- return yield* progress.withTask(
139
+ return yield* progress.runTask(
121
140
  Effect.gen(function* () {
122
141
  const taskId = yield* Task;
123
- return yield* Effect.forEach(
124
- iterable,
125
- (item, index) =>
126
- Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
127
- {
128
- concurrency: options.concurrency,
129
- batching: options.batching,
130
- discard: options.discard,
131
- concurrentFinalizers: options.concurrentFinalizers,
132
- },
142
+ const exit = yield* Effect.exit(
143
+ Effect.forEach(
144
+ iterable,
145
+ (item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
146
+ {
147
+ concurrency: options.concurrency,
148
+ batching: options.batching,
149
+ discard: options.discard,
150
+ concurrentFinalizers: options.concurrentFinalizers,
151
+ },
152
+ ),
133
153
  );
154
+
155
+ if (Exit.isSuccess(exit)) {
156
+ yield* progress.completeTask(taskId);
157
+ } else {
158
+ yield* progress.failTask(taskId);
159
+ }
160
+
161
+ return yield* Exit.match(exit, {
162
+ onFailure: Effect.failCause,
163
+ onSuccess: Effect.succeed,
164
+ });
134
165
  }),
135
166
  {
136
167
  description: options.description,
package/src/console.ts CHANGED
@@ -12,7 +12,7 @@ export const makeProgressConsole = (
12
12
 
13
13
  const delegate = (effect: Effect.Effect<void, never, never>) => effect;
14
14
 
15
- return {
15
+ return Console.Console.of({
16
16
  [Console.TypeId]: Console.TypeId,
17
17
  assert(condition, ...args) {
18
18
  return condition ? Effect.void : log("Assertion failed:", ...args);
@@ -81,5 +81,5 @@ export const makeProgressConsole = (
81
81
  unsafeLog(...args);
82
82
  },
83
83
  },
84
- };
84
+ });
85
85
  };
package/src/renderer.ts CHANGED
@@ -5,8 +5,8 @@ import {
5
5
  ProgressBarColorsSchema,
6
6
  } from "./colors";
7
7
  import type { ProgressTerminalService } from "./terminal";
8
- import type { ProgressBarConfigShape, RendererConfigShape } from "./types";
9
- import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
8
+ import type { ProgressBarConfigShape, RendererConfigShape, TaskStore } from "./types";
9
+ import { DeterminateTaskUnits, TaskSnapshot } from "./types";
10
10
 
11
11
  const HIDE_CURSOR = "\x1b[?25l";
12
12
  const SHOW_CURSOR = "\x1b[?25h";
@@ -33,7 +33,7 @@ const buildTaskLine = (
33
33
  tick: number,
34
34
  colors: CompiledProgressBarColors,
35
35
  ): string => {
36
- const progressbar = snapshot.progressbar;
36
+ const progressbar = snapshot.config;
37
37
  const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
38
38
 
39
39
  if (snapshot.status === "failed") {
@@ -57,31 +57,8 @@ const buildTaskLine = (
57
57
  return `${prefix}${colors.spinner(frame)}`;
58
58
  };
59
59
 
60
- const orderTasksForRender = (
61
- tasks: ReadonlyArray<TaskSnapshot>,
62
- ): ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }> => {
63
- const byParent = new Map<number | null, Array<TaskSnapshot>>();
64
- for (const task of tasks) {
65
- const bucket = byParent.get(task.parentId) ?? [];
66
- bucket.push(task);
67
- byParent.set(task.parentId, bucket);
68
- }
69
-
70
- const ordered: Array<{ snapshot: TaskSnapshot; depth: number }> = [];
71
- const visit = (parentId: number | null, depth: number) => {
72
- const children = byParent.get(parentId) ?? [];
73
- for (const child of children) {
74
- ordered.push({ snapshot: child, depth });
75
- visit(child.id, depth + 1);
76
- }
77
- };
78
-
79
- visit(null, 0);
80
- return ordered;
81
- };
82
-
83
60
  export const runProgressServiceRenderer = (
84
- tasksRef: Ref.Ref<Map<TaskId, TaskSnapshot>>,
61
+ storeRef: Ref.Ref<TaskStore>,
85
62
  logsRef: Ref.Ref<ReadonlyArray<string>>,
86
63
  pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
87
64
  dirtyRef: Ref.Ref<boolean>,
@@ -187,14 +164,16 @@ export const runProgressServiceRenderer = (
187
164
  const renderFrame = (mode: "tick" | "final") =>
188
165
  Effect.gen(function* () {
189
166
  const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
190
- const snapshots = Array.from((yield* Ref.get(tasksRef)).values()).filter(
191
- (task) => !(task.transient && task.status !== "running"),
192
- );
193
- const ordered = orderTasksForRender(snapshots);
167
+ const store = yield* Ref.get(storeRef);
168
+ const ordered = store.renderOrder.flatMap((row) => {
169
+ const snapshot = store.tasks.get(row.id);
170
+ if (!snapshot || (snapshot.transient && snapshot.status !== "running")) return [];
171
+ return [{ snapshot, depth: row.depth }];
172
+ });
194
173
  const frameTick = mode === "final" ? tick + 1 : tick;
195
174
  const taskLines = ordered.map(({ snapshot, depth }) => {
196
175
  const lineTick = isTTY ? frameTick : 0;
197
- return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.progressbar));
176
+ return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.config));
198
177
  });
199
178
 
200
179
  if (isTTY) {
@@ -248,7 +227,7 @@ export const runProgressServiceRenderer = (
248
227
 
249
228
  while (true) {
250
229
  const dirty = yield* Ref.getAndSet(dirtyRef, false);
251
- const tasks = Array.from((yield* Ref.get(tasksRef)).values()).filter(
230
+ const tasks = Array.from((yield* Ref.get(storeRef)).tasks.values()).filter(
252
231
  (task) => !(task.transient && task.status !== "running"),
253
232
  );
254
233
  const hasActiveSpinners = tasks.some(
package/src/runtime.ts CHANGED
@@ -6,7 +6,13 @@ import type { PartialDeep } from "type-fest";
6
6
  import { makeProgressConsole } from "./console";
7
7
  import { runProgressServiceRenderer } from "./renderer";
8
8
  import { ProgressTerminal } from "./terminal";
9
- import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
9
+ import type {
10
+ AddTaskOptions,
11
+ ProgressService,
12
+ RenderRow,
13
+ TaskStore,
14
+ UpdateTaskOptions,
15
+ } from "./types";
10
16
  import {
11
17
  decodeProgressBarConfigSync,
12
18
  decodeRendererConfigSync,
@@ -20,7 +26,6 @@ import {
20
26
  TaskId,
21
27
  TaskSnapshot,
22
28
  } from "./types";
23
- import { inferTotal } from "./utils";
24
29
 
25
30
  const mergeConfig = <T extends Record<PropertyKey, any>>(
26
31
  base: T,
@@ -76,10 +81,39 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
76
81
  status: snapshot.status,
77
82
  transient: options.transient ?? snapshot.transient,
78
83
  units,
79
- progressbar: snapshot.progressbar,
84
+ config: snapshot.config,
80
85
  });
81
86
  };
82
87
 
88
+ const findInsertionIndex = (
89
+ renderOrder: ReadonlyArray<RenderRow>,
90
+ parentId: TaskId | null,
91
+ ): { index: number; depth: number } => {
92
+ if (parentId === null) {
93
+ return { index: renderOrder.length, depth: 0 };
94
+ }
95
+ const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
96
+ if (parentIdx === -1) return { index: renderOrder.length, depth: 0 };
97
+ const parentDepth = renderOrder[parentIdx]!.depth;
98
+ let i = parentIdx + 1;
99
+ while (i < renderOrder.length && renderOrder[i]!.depth > parentDepth) i++;
100
+ return { index: i, depth: parentDepth + 1 };
101
+ };
102
+
103
+ const removeFromRenderOrder = (
104
+ renderOrder: ReadonlyArray<RenderRow>,
105
+ taskId: TaskId,
106
+ ): ReadonlyArray<RenderRow> => {
107
+ const idx = renderOrder.findIndex((row) => row.id === taskId);
108
+ if (idx === -1) return renderOrder;
109
+ const taskDepth = renderOrder[idx]!.depth;
110
+ let end = idx + 1;
111
+ while (end < renderOrder.length && renderOrder[end]!.depth > taskDepth) end++;
112
+ const next = [...renderOrder];
113
+ next.splice(idx, end - idx);
114
+ return next;
115
+ };
116
+
83
117
  const makeProgressService = Effect.gen(function* () {
84
118
  const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
85
119
  const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
@@ -101,7 +135,10 @@ const makeProgressService = Effect.gen(function* () {
101
135
  const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
102
136
 
103
137
  const nextTaskIdRef = yield* Ref.make(0);
104
- const tasksRef = yield* Ref.make(new Map<TaskId, TaskSnapshot>());
138
+ const storeRef = yield* Ref.make<TaskStore>({
139
+ tasks: new Map<TaskId, TaskSnapshot>(),
140
+ renderOrder: [],
141
+ });
105
142
  const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
106
143
  const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
107
144
  const dirtyRef = yield* Ref.make(true);
@@ -110,7 +147,7 @@ const makeProgressService = Effect.gen(function* () {
110
147
 
111
148
  yield* Effect.forkIn(
112
149
  runProgressServiceRenderer(
113
- tasksRef,
150
+ storeRef,
114
151
  logsRef,
115
152
  pendingLogsRef,
116
153
  dirtyRef,
@@ -126,7 +163,7 @@ const makeProgressService = Effect.gen(function* () {
126
163
 
127
164
  const addTask = (options: AddTaskOptions) =>
128
165
  Effect.gen(function* () {
129
- const parentId =
166
+ const resolvedParentId =
130
167
  options.parentId === undefined
131
168
  ? yield* FiberRef.get(currentParentRef)
132
169
  : Option.some(options.parentId);
@@ -135,27 +172,33 @@ const makeProgressService = Effect.gen(function* () {
135
172
  options.total === undefined || options.total <= 0
136
173
  ? new IndeterminateTaskUnits({ spinnerFrame: 0 })
137
174
  : new DeterminateTaskUnits({ completed: 0, total: Math.max(0, options.total) });
138
- const tasks = yield* Ref.get(tasksRef);
139
- const parentSnapshot = Option.isSome(parentId) ? tasks.get(parentId.value) : undefined;
140
- const inheritedProgressBarConfig = parentSnapshot?.progressbar ?? progressBarConfig;
175
+ const store = yield* Ref.get(storeRef);
176
+ const parentSnapshot = Option.isSome(resolvedParentId)
177
+ ? store.tasks.get(resolvedParentId.value)
178
+ : undefined;
179
+ const inheritedProgressBarConfig = parentSnapshot?.config ?? progressBarConfig;
141
180
  const resolvedProgressBarConfig = decodeProgressBarConfigSync(
142
181
  mergeConfig(inheritedProgressBarConfig, options.progressbar),
143
182
  );
144
183
 
184
+ const parentIdValue = Option.getOrNull(resolvedParentId);
145
185
  const snapshot = new TaskSnapshot({
146
186
  id: taskId,
147
- parentId: Option.getOrNull(parentId),
187
+ parentId: parentIdValue,
148
188
  description: options.description,
149
189
  status: "running",
150
190
  transient: options.transient ?? false,
151
191
  units,
152
- progressbar: resolvedProgressBarConfig,
192
+ config: resolvedProgressBarConfig,
153
193
  });
154
194
 
155
- yield* Ref.update(tasksRef, (tasks) => {
156
- const next = new Map(tasks);
157
- next.set(taskId, snapshot);
158
- return next;
195
+ yield* Ref.update(storeRef, (s) => {
196
+ const nextTasks = new Map(s.tasks);
197
+ nextTasks.set(taskId, snapshot);
198
+ const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
199
+ const nextOrder = [...s.renderOrder];
200
+ nextOrder.splice(index, 0, { id: taskId, depth });
201
+ return { tasks: nextTasks, renderOrder: nextOrder };
159
202
  });
160
203
  yield* markDirty;
161
204
 
@@ -163,25 +206,19 @@ const makeProgressService = Effect.gen(function* () {
163
206
  });
164
207
 
165
208
  const updateTask = (taskId: TaskId, options: UpdateTaskOptions) =>
166
- Ref.update(tasksRef, (tasks) => {
167
- const snapshot = tasks.get(taskId);
168
- if (!snapshot) {
169
- return tasks;
170
- }
171
-
172
- const next = new Map(tasks);
173
- next.set(taskId, updatedSnapshot(snapshot, options));
174
- return next;
209
+ Ref.update(storeRef, (store) => {
210
+ const snapshot = store.tasks.get(taskId);
211
+ if (!snapshot) return store;
212
+ const nextTasks = new Map(store.tasks);
213
+ nextTasks.set(taskId, updatedSnapshot(snapshot, options));
214
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
175
215
  }).pipe(Effect.zipRight(markDirty));
176
216
 
177
217
  const advanceTask = (taskId: TaskId, amount = 1) =>
178
- Ref.update(tasksRef, (tasks) => {
179
- const snapshot = tasks.get(taskId);
180
- if (!snapshot) {
181
- return tasks;
182
- }
218
+ Ref.update(storeRef, (store) => {
219
+ const snapshot = store.tasks.get(taskId);
220
+ if (!snapshot) return store;
183
221
 
184
- const next = new Map(tasks);
185
222
  const units =
186
223
  snapshot.units._tag === "DeterminateTaskUnits"
187
224
  ? new DeterminateTaskUnits({
@@ -192,7 +229,8 @@ const makeProgressService = Effect.gen(function* () {
192
229
  spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount),
193
230
  });
194
231
 
195
- next.set(
232
+ const nextTasks = new Map(store.tasks);
233
+ nextTasks.set(
196
234
  taskId,
197
235
  new TaskSnapshot({
198
236
  id: snapshot.id,
@@ -201,27 +239,25 @@ const makeProgressService = Effect.gen(function* () {
201
239
  status: snapshot.status,
202
240
  transient: snapshot.transient,
203
241
  units,
204
- progressbar: snapshot.progressbar,
242
+ config: snapshot.config,
205
243
  }),
206
244
  );
207
245
 
208
- return next;
246
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
209
247
  }).pipe(Effect.zipRight(markDirty));
210
248
 
211
249
  const completeTask = (taskId: TaskId) =>
212
- Ref.update(tasksRef, (tasks) => {
213
- const snapshot = tasks.get(taskId);
214
- if (!snapshot) {
215
- return tasks;
216
- }
250
+ Ref.update(storeRef, (store) => {
251
+ const snapshot = store.tasks.get(taskId);
252
+ if (!snapshot) return store;
217
253
 
218
- const next = new Map(tasks);
254
+ const nextTasks = new Map(store.tasks);
219
255
  if (snapshot.transient) {
220
- next.delete(taskId);
221
- return next;
256
+ nextTasks.delete(taskId);
257
+ return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
222
258
  }
223
259
 
224
- next.set(
260
+ nextTasks.set(
225
261
  taskId,
226
262
  new TaskSnapshot({
227
263
  id: snapshot.id,
@@ -236,26 +272,24 @@ const makeProgressService = Effect.gen(function* () {
236
272
  total: snapshot.units.total,
237
273
  })
238
274
  : snapshot.units,
239
- progressbar: snapshot.progressbar,
275
+ config: snapshot.config,
240
276
  }),
241
277
  );
242
- return next;
278
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
243
279
  }).pipe(Effect.zipRight(markDirty));
244
280
 
245
281
  const failTask = (taskId: TaskId) =>
246
- Ref.update(tasksRef, (tasks) => {
247
- const snapshot = tasks.get(taskId);
248
- if (!snapshot) {
249
- return tasks;
250
- }
282
+ Ref.update(storeRef, (store) => {
283
+ const snapshot = store.tasks.get(taskId);
284
+ if (!snapshot) return store;
251
285
 
252
- const next = new Map(tasks);
286
+ const nextTasks = new Map(store.tasks);
253
287
  if (snapshot.transient) {
254
- next.delete(taskId);
255
- return next;
288
+ nextTasks.delete(taskId);
289
+ return { tasks: nextTasks, renderOrder: removeFromRenderOrder(store.renderOrder, taskId) };
256
290
  }
257
291
 
258
- next.set(
292
+ nextTasks.set(
259
293
  taskId,
260
294
  new TaskSnapshot({
261
295
  id: snapshot.id,
@@ -264,10 +298,10 @@ const makeProgressService = Effect.gen(function* () {
264
298
  status: "failed",
265
299
  transient: snapshot.transient,
266
300
  units: snapshot.units,
267
- progressbar: snapshot.progressbar,
301
+ config: snapshot.config,
268
302
  }),
269
303
  );
270
- return next;
304
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
271
305
  }).pipe(Effect.zipRight(markDirty));
272
306
 
273
307
  const appendLog = (args: ReadonlyArray<unknown>) =>
@@ -302,11 +336,11 @@ const makeProgressService = Effect.gen(function* () {
302
336
  const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
303
337
 
304
338
  const getTask = (taskId: TaskId) =>
305
- Ref.get(tasksRef).pipe(Effect.map((tasks) => Option.fromNullable(tasks.get(taskId))));
339
+ Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
306
340
 
307
- const listTasks = Ref.get(tasksRef).pipe(Effect.map((tasks) => Array.from(tasks.values())));
341
+ const listTasks = Ref.get(storeRef).pipe(Effect.map((store) => Array.from(store.tasks.values())));
308
342
 
309
- const withTask: ProgressService["withTask"] = dual(
343
+ const runTask: ProgressService["runTask"] = dual(
310
344
  2,
311
345
  <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
312
346
  Effect.gen(function* () {
@@ -321,45 +355,39 @@ const makeProgressService = Effect.gen(function* () {
321
355
  transient: options.transient ?? Option.isSome(resolvedParentId),
322
356
  });
323
357
 
324
- const exit = yield* Effect.exit(
325
- Effect.locally(
326
- Effect.withConsole(
327
- Effect.provideService(effect, Task, taskId),
328
- makeProgressConsole(log, outerConsole),
329
- ),
330
- currentParentRef,
331
- Option.some(taskId),
358
+ return yield* Effect.locally(
359
+ Effect.withConsole(
360
+ Effect.provideService(effect, Task, taskId),
361
+ makeProgressConsole(log, outerConsole),
332
362
  ),
363
+ currentParentRef,
364
+ Option.some(taskId),
333
365
  );
334
-
335
- if (Exit.isSuccess(exit)) {
336
- yield* completeTask(taskId);
337
- } else {
338
- yield* failTask(taskId);
339
- }
340
-
341
- return yield* Exit.match(exit, {
342
- onFailure: Effect.failCause,
343
- onSuccess: Effect.succeed,
344
- });
345
366
  }),
346
367
  );
347
368
 
348
- const trackIterable: ProgressService["trackIterable"] = (iterable, options, f) =>
349
- withTask(
350
- Effect.gen(function* () {
351
- const taskId = yield* Task;
352
- return yield* Effect.forEach(iterable, (item, index) =>
353
- Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
354
- );
355
- }),
356
- {
357
- description: options.description,
358
- total: options.total ?? inferTotal(iterable),
359
- transient: options.transient,
360
- progressbar: options.progressbar,
361
- },
362
- );
369
+ const withTask: ProgressService["withTask"] = dual(
370
+ 2,
371
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
372
+ runTask(
373
+ Effect.gen(function* () {
374
+ const taskId = yield* Task;
375
+ const exit = yield* Effect.exit(effect);
376
+
377
+ if (Exit.isSuccess(exit)) {
378
+ yield* completeTask(taskId);
379
+ } else {
380
+ yield* failTask(taskId);
381
+ }
382
+
383
+ return yield* Exit.match(exit, {
384
+ onFailure: Effect.failCause,
385
+ onSuccess: Effect.succeed,
386
+ });
387
+ }),
388
+ options,
389
+ ),
390
+ );
363
391
 
364
392
  const service: ProgressService = {
365
393
  addTask,
@@ -370,8 +398,8 @@ const makeProgressService = Effect.gen(function* () {
370
398
  log,
371
399
  getTask,
372
400
  listTasks,
401
+ runTask,
373
402
  withTask,
374
- trackIterable,
375
403
  };
376
404
 
377
405
  return Progress.of(service);
package/src/terminal.ts CHANGED
@@ -5,9 +5,7 @@ export interface ProgressTerminalService {
5
5
  readonly stderrRows: Effect.Effect<number | undefined>;
6
6
  readonly stderrColumns: Effect.Effect<number | undefined>;
7
7
  readonly writeStderr: (text: string) => Effect.Effect<void>;
8
- readonly withRawInputCapture: <A, E, R>(
9
- effect: Effect.Effect<A, E, R>,
10
- ) => Effect.Effect<A, E, R>;
8
+ readonly withRawInputCapture: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
11
9
  }
12
10
 
13
11
  const withRawInputCapture: ProgressTerminalService["withRawInputCapture"] = (effect) =>
package/src/types.ts CHANGED
@@ -102,9 +102,19 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
102
102
  status: TaskStatusSchema,
103
103
  transient: Schema.Boolean,
104
104
  units: TaskUnitsSchema,
105
- progressbar: ProgressBarConfigSchema,
105
+ config: ProgressBarConfigSchema,
106
106
  }) {}
107
107
 
108
+ export interface RenderRow {
109
+ readonly id: TaskId;
110
+ readonly depth: number;
111
+ }
112
+
113
+ export interface TaskStore {
114
+ readonly tasks: Map<TaskId, TaskSnapshot>;
115
+ readonly renderOrder: ReadonlyArray<RenderRow>;
116
+ }
117
+
108
118
  export interface ProgressService {
109
119
  readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
110
120
  readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
@@ -114,6 +124,15 @@ export interface ProgressService {
114
124
  readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
115
125
  readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
116
126
  readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
127
+ readonly runTask: {
128
+ <A, E, R>(
129
+ effect: Effect.Effect<A, E, R>,
130
+ options: AddTaskOptions,
131
+ ): Effect.Effect<A, E, Exclude<R, Task>>;
132
+ <A, E, R>(
133
+ options: AddTaskOptions,
134
+ ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
135
+ };
117
136
  readonly withTask: {
118
137
  <A, E, R>(
119
138
  effect: Effect.Effect<A, E, R>,
@@ -123,11 +142,6 @@ export interface ProgressService {
123
142
  options: AddTaskOptions,
124
143
  ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
125
144
  };
126
- readonly trackIterable: <A, B, E, R>(
127
- iterable: Iterable<A>,
128
- options: TrackOptions,
129
- f: (item: A, index: number) => Effect.Effect<B, E, R>,
130
- ) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Task>>;
131
145
  }
132
146
 
133
147
  export class Task extends Context.Tag("stromseng.dev/Task")<Task, TaskId>() {}