effective-progress 0.11.1 → 0.12.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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
2
- import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
2
+ import { Brand, Cause, Clock, Context, Effect, Exit, Layer, Option, Queue, Schema } from "effect";
3
3
  import { dual } from "effect/Function";
4
4
  import { Box, Text, render, useBoxMetrics } from "ink";
5
- import { createContext, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
5
+ import { createContext, use, useEffect, useMemo, useReducer, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
  import cliSpinners from "cli-spinners";
8
8
  import stringWidth from "fast-string-width";
@@ -10,8 +10,12 @@ import stringWidth from "fast-string-width";
10
10
  //#region src/types.ts
11
11
  const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
12
12
  const TaskId = Brand.nominal();
13
- const TaskStatusSchema = Schema.Literal("running", "done", "failed");
14
- const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
13
+ const TaskStatusSchema = Schema.Literals([
14
+ "running",
15
+ "done",
16
+ "failed"
17
+ ]);
18
+ const TaskCountDisplaySchema = Schema.Literals(["processedOnly", "detailed"]);
15
19
  const TaskUnitsSchema = Schema.Struct({
16
20
  succeeded: Schema.Number,
17
21
  failed: Schema.Number,
@@ -36,7 +40,7 @@ const TaskSnapshotSchema = Schema.Struct({
36
40
  metadata: Schema.Unknown
37
41
  });
38
42
  const TaskSnapshot = (snapshot) => snapshot;
39
- var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
43
+ var Task = class extends Context.Service()("stromseng.dev/effective-progress/Task") {};
40
44
  var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
41
45
  taskId: TaskIdSchema,
42
46
  parentId: Schema.NullOr(TaskIdSchema),
@@ -58,27 +62,30 @@ var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
58
62
  var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
59
63
  taskId: TaskIdSchema,
60
64
  amount: Schema.Number,
61
- kind: Schema.Literal("succeeded", "failed")
65
+ kind: Schema.Literals(["succeeded", "failed"])
62
66
  }) {};
63
67
  var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
64
68
  var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
65
69
  var TaskRemovedEvent = class extends Schema.TaggedClass()("TaskRemoved", { taskId: TaskIdSchema }) {};
66
- const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskRemovedEvent);
70
+ const ProgressTaskEventSchema = Schema.Union([
71
+ TaskAddedEvent,
72
+ TaskUpdatedEvent,
73
+ TaskAdvancedEvent,
74
+ TaskCompletedEvent,
75
+ TaskFailedEvent,
76
+ TaskRemovedEvent
77
+ ]);
67
78
  const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
68
79
 
69
80
  //#endregion
70
- //#region src/renderer/store.ts
81
+ //#region src/services/store/store.ts
71
82
  const ETA_SAMPLE_WINDOW_MILLIS = 3e4;
72
83
  const ETA_SAMPLE_MAX_LENGTH = 1e3;
73
84
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
74
- const sanitizeTotalOnAdd = (total) => {
85
+ const sanitizeTotal = (total) => {
75
86
  if (total === void 0) return;
76
87
  return total < 0 ? void 0 : total;
77
88
  };
78
- const sanitizeTotalOnUpdate = (nextTotal) => {
79
- if (nextTotal === void 0) return;
80
- return nextTotal < 0 ? void 0 : nextTotal;
81
- };
82
89
  const normalizeUnits = (counts) => {
83
90
  const succeeded = Math.max(0, counts.succeeded);
84
91
  const failed = Math.max(0, counts.failed);
@@ -103,12 +110,13 @@ const appendProgressSample = (samples, now, processed) => {
103
110
  const previousSamples = samples ?? [];
104
111
  if (previousSamples.at(-1)?.processed === processed) return previousSamples;
105
112
  const windowStart = now - ETA_SAMPLE_WINDOW_MILLIS;
106
- const nextSamples = [...previousSamples, {
113
+ const appendedLength = previousSamples.length + 1;
114
+ let firstRetainedIndex = Math.max(0, appendedLength - ETA_SAMPLE_MAX_LENGTH);
115
+ while (firstRetainedIndex + 1 < previousSamples.length && previousSamples[firstRetainedIndex + 1].timestamp < windowStart) firstRetainedIndex++;
116
+ return [...previousSamples.slice(firstRetainedIndex), {
107
117
  timestamp: now,
108
118
  processed
109
119
  }];
110
- while (nextSamples.length > 2 && (nextSamples.length > ETA_SAMPLE_MAX_LENGTH || nextSamples[1].timestamp < windowStart)) nextSamples.shift();
111
- return nextSamples;
112
120
  };
113
121
  /** Applies mutable task fields and records a progress sample when the processed count changes. */
114
122
  const updatedSnapshot = (snapshot, options, now) => {
@@ -116,7 +124,7 @@ const updatedSnapshot = (snapshot, options, now) => {
116
124
  const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
117
125
  succeeded: options.succeeded ?? currentUnits.succeeded,
118
126
  failed: options.failed ?? currentUnits.failed,
119
- total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
127
+ total: hasExplicitTotal(options) ? sanitizeTotal(options.total) : currentUnits.total
120
128
  });
121
129
  return TaskSnapshot({
122
130
  ...snapshot,
@@ -163,8 +171,19 @@ const subtreeTaskIds = (renderOrder, taskId) => {
163
171
  while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
164
172
  return renderOrder.slice(idx, end).map((row) => row.id);
165
173
  };
174
+ const removeTransientSubtree = (current, nextTasks, taskId) => {
175
+ const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
176
+ for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
177
+ const nextColumns = new Map(current.columns);
178
+ for (const removedTaskId of removedTaskIds) nextColumns.delete(removedTaskId);
179
+ return {
180
+ removedTaskIds,
181
+ renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
182
+ columns: nextColumns
183
+ };
184
+ };
166
185
  const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 100;
167
- const makeProgressRenderStore = () => {
186
+ const makeProgressStoreRuntime = (publishQueue) => {
168
187
  let nextTaskId = 0;
169
188
  let state = {
170
189
  tasks: /* @__PURE__ */ new Map(),
@@ -177,13 +196,13 @@ const makeProgressRenderStore = () => {
177
196
  events: []
178
197
  };
179
198
  let hasPendingPublish = false;
180
- let lastPublishAt = 0;
181
- let publishTimeout;
199
+ let lastPublishAt = -SNAPSHOT_PUBLISH_INTERVAL_MILLIS;
200
+ let latestObservedAt = 0;
182
201
  const listeners = /* @__PURE__ */ new Set();
183
202
  const notifyListeners = () => {
184
203
  for (const listener of listeners) listener();
185
204
  };
186
- const publishNow = () => {
205
+ const publishNow = (publishedAt) => {
187
206
  const nextPublication = {
188
207
  snapshot: state,
189
208
  events: [...pendingEvents]
@@ -192,350 +211,351 @@ const makeProgressRenderStore = () => {
192
211
  publishedPublication = nextPublication;
193
212
  notifyListeners();
194
213
  pendingEvents = [];
195
- lastPublishAt = Date.now();
196
- };
197
- const clearScheduledPublish = () => {
198
- if (publishTimeout === void 0) return;
199
- clearTimeout(publishTimeout);
200
- publishTimeout = void 0;
214
+ lastPublishAt = publishedAt;
201
215
  };
202
- const schedulePublish = () => {
203
- if (!hasPendingPublish) return;
204
- const now = Date.now();
216
+ const publisherLoop = Effect.forever(Effect.gen(function* () {
217
+ yield* Queue.take(publishQueue);
218
+ const now = yield* Clock.currentTimeMillis;
205
219
  const waitMillis = Math.max(0, SNAPSHOT_PUBLISH_INTERVAL_MILLIS - (now - lastPublishAt));
206
- if (waitMillis === 0) {
207
- clearScheduledPublish();
208
- publishNow();
220
+ if (waitMillis > 0) yield* Effect.sleep(waitMillis);
221
+ if (!hasPendingPublish) return;
222
+ publishNow(yield* Clock.currentTimeMillis);
223
+ }));
224
+ const schedulePublish = Effect.gen(function* () {
225
+ if (!hasPendingPublish) return;
226
+ const now = yield* Clock.currentTimeMillis;
227
+ if (Math.max(0, SNAPSHOT_PUBLISH_INTERVAL_MILLIS - (now - lastPublishAt)) === 0) {
228
+ publishNow(now);
209
229
  return;
210
230
  }
211
- if (publishTimeout !== void 0) return;
212
- publishTimeout = setTimeout(() => {
213
- publishTimeout = void 0;
214
- if (!hasPendingPublish) return;
215
- publishNow();
216
- }, waitMillis);
217
- };
218
- const publish = (update) => {
219
- if (update.state === state) return;
231
+ yield* Queue.offer(publishQueue, void 0);
232
+ });
233
+ const publish = (update, now) => {
234
+ if (update.state === state) return Effect.void;
235
+ latestObservedAt = now;
220
236
  state = update.state;
221
237
  if (update.events.length > 0) pendingEvents.push(...update.events);
222
238
  hasPendingPublish = true;
223
- schedulePublish();
239
+ return schedulePublish;
224
240
  };
225
- const updateState = (transform) => {
226
- publish(transform(state));
241
+ const updateState = (transform, now) => {
242
+ return publish(transform(state), now);
227
243
  };
228
244
  return {
229
- getSnapshot: () => publishedPublication,
230
- subscribe: (listener) => {
231
- listeners.add(listener);
232
- return () => {
233
- listeners.delete(listener);
234
- };
235
- },
236
- flush: () => {
237
- if (!hasPendingPublish) return;
238
- clearScheduledPublish();
239
- publishNow();
240
- },
241
- addTask: (options) => Effect.gen(function* () {
242
- const taskId = TaskId(++nextTaskId);
243
- const units = normalizeUnits({
244
- succeeded: 0,
245
- failed: 0,
246
- total: sanitizeTotalOnAdd(options.total)
247
- });
248
- const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
249
- const now = yield* Clock.currentTimeMillis;
250
- const parentId = options.parentId ?? null;
251
- const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
252
- const task = TaskSnapshot({
253
- id: taskId,
254
- parentId,
255
- description: options.description,
256
- status: "running",
257
- countDisplay,
258
- transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
259
- units,
260
- startedAt: now,
261
- completedAt: null,
262
- progressSamples: [{
263
- timestamp: now,
264
- processed: units.processed
265
- }],
266
- metadata: options.metadata
267
- });
268
- updateState((current) => {
269
- const nextTasks = new Map(current.tasks);
270
- nextTasks.set(taskId, task);
271
- const { index, depth } = findInsertionIndex(current.renderOrder, parentId);
272
- const nextRenderOrder = [...current.renderOrder];
273
- nextRenderOrder.splice(index, 0, {
274
- id: taskId,
275
- depth
276
- });
277
- return {
278
- state: {
279
- tasks: nextTasks,
280
- renderOrder: nextRenderOrder,
281
- columns: options.columns ? new Map(current.columns).set(taskId, options.columns) : current.columns
282
- },
283
- events: [new TaskAddedEvent({
284
- taskId,
285
- parentId,
286
- description: task.description,
287
- total: task.units.total,
288
- transient: task.transient,
289
- countDisplay: task.countDisplay
290
- })]
291
- };
292
- });
293
- return taskId;
294
- }),
295
- updateTask: (taskId, options) => Effect.gen(function* () {
296
- const now = yield* Clock.currentTimeMillis;
297
- updateState((current) => {
298
- const currentTask = current.tasks.get(taskId);
299
- if (!currentTask) return {
300
- state: current,
301
- events: []
302
- };
303
- const nextTask = updatedSnapshot(currentTask, options, now);
304
- const nextTasks = new Map(current.tasks);
305
- nextTasks.set(taskId, nextTask);
306
- const events = [new TaskUpdatedEvent({
307
- taskId,
308
- description: options.description ?? void 0,
309
- succeeded: options.succeeded ?? void 0,
310
- failed: options.failed ?? void 0,
311
- processed: options.succeeded !== void 0 || options.failed !== void 0 ? nextTask.units.processed : void 0,
312
- total: hasExplicitTotal(options) ? nextTask.units.total : void 0,
313
- transient: options.transient ?? void 0,
314
- countDisplay: options.countDisplay ?? void 0
315
- })];
316
- if (options.transient !== void 0) for (const candidateId of subtreeTaskIds(current.renderOrder, taskId).slice(1)) {
317
- const candidate = current.tasks.get(candidateId);
318
- if (!candidate) continue;
319
- const nextCandidate = TaskSnapshot({
320
- ...candidate,
321
- transient: nextTask.transient
322
- });
323
- nextTasks.set(candidateId, nextCandidate);
324
- events.push(new TaskUpdatedEvent({
325
- taskId: candidateId,
326
- transient: nextCandidate.transient
327
- }));
328
- }
329
- return {
330
- state: {
331
- tasks: nextTasks,
332
- renderOrder: current.renderOrder,
333
- columns: current.columns
334
- },
335
- events
336
- };
337
- });
338
- }),
339
- incrementSucceeded: (taskId, amount = 1) => Effect.gen(function* () {
340
- const now = yield* Clock.currentTimeMillis;
341
- updateState((current) => {
342
- const currentTask = current.tasks.get(taskId);
343
- if (!currentTask) return {
344
- state: current,
345
- events: []
245
+ store: {
246
+ getSnapshot: () => publishedPublication,
247
+ subscribe: (listener) => {
248
+ listeners.add(listener);
249
+ return () => {
250
+ listeners.delete(listener);
346
251
  };
252
+ },
253
+ flush: () => {
254
+ if (!hasPendingPublish) return;
255
+ publishNow(latestObservedAt);
256
+ },
257
+ addTask: (options) => Effect.gen(function* () {
258
+ const taskId = TaskId(++nextTaskId);
347
259
  const units = normalizeUnits({
348
- succeeded: currentTask.units.succeeded + amount,
349
- failed: currentTask.units.failed,
350
- total: currentTask.units.total
260
+ succeeded: 0,
261
+ failed: 0,
262
+ total: sanitizeTotal(options.total)
351
263
  });
352
- const nextTasks = new Map(current.tasks);
353
- nextTasks.set(taskId, TaskSnapshot({
354
- ...currentTask,
264
+ const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
265
+ const now = yield* Clock.currentTimeMillis;
266
+ const parentId = options.parentId ?? null;
267
+ const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
268
+ const task = TaskSnapshot({
269
+ id: taskId,
270
+ parentId,
271
+ description: options.description,
272
+ status: "running",
273
+ countDisplay,
274
+ transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
355
275
  units,
356
- progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
357
- }));
358
- return {
359
- state: {
360
- tasks: nextTasks,
361
- renderOrder: current.renderOrder,
362
- columns: current.columns
363
- },
364
- events: [new TaskAdvancedEvent({
365
- taskId,
366
- amount,
367
- kind: "succeeded"
368
- })]
369
- };
370
- });
371
- }),
372
- incrementFailed: (taskId, amount = 1) => Effect.gen(function* () {
373
- const now = yield* Clock.currentTimeMillis;
374
- updateState((current) => {
375
- const currentTask = current.tasks.get(taskId);
376
- if (!currentTask) return {
377
- state: current,
378
- events: []
379
- };
380
- const units = normalizeUnits({
381
- succeeded: currentTask.units.succeeded,
382
- failed: currentTask.units.failed + amount,
383
- total: currentTask.units.total
276
+ startedAt: now,
277
+ completedAt: null,
278
+ progressSamples: [{
279
+ timestamp: now,
280
+ processed: units.processed
281
+ }],
282
+ metadata: options.metadata
384
283
  });
385
- const nextTasks = new Map(current.tasks);
386
- nextTasks.set(taskId, TaskSnapshot({
387
- ...currentTask,
388
- units,
389
- progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
390
- }));
391
- return {
392
- state: {
393
- tasks: nextTasks,
394
- renderOrder: current.renderOrder,
395
- columns: current.columns
396
- },
397
- events: [new TaskAdvancedEvent({
284
+ yield* updateState((current) => {
285
+ const nextTasks = new Map(current.tasks);
286
+ nextTasks.set(taskId, task);
287
+ const { index, depth } = findInsertionIndex(current.renderOrder, parentId);
288
+ const nextRenderOrder = [...current.renderOrder];
289
+ nextRenderOrder.splice(index, 0, {
290
+ id: taskId,
291
+ depth
292
+ });
293
+ return {
294
+ state: {
295
+ tasks: nextTasks,
296
+ renderOrder: nextRenderOrder,
297
+ columns: options.columns ? new Map(current.columns).set(taskId, options.columns) : current.columns
298
+ },
299
+ events: [new TaskAddedEvent({
300
+ taskId,
301
+ parentId,
302
+ description: task.description,
303
+ total: task.units.total,
304
+ transient: task.transient,
305
+ countDisplay: task.countDisplay
306
+ })]
307
+ };
308
+ }, now);
309
+ return taskId;
310
+ }),
311
+ updateTask: (taskId, options) => Effect.gen(function* () {
312
+ const now = yield* Clock.currentTimeMillis;
313
+ yield* updateState((current) => {
314
+ const currentTask = current.tasks.get(taskId);
315
+ if (!currentTask) return {
316
+ state: current,
317
+ events: []
318
+ };
319
+ const nextTask = updatedSnapshot(currentTask, options, now);
320
+ const nextTasks = new Map(current.tasks);
321
+ nextTasks.set(taskId, nextTask);
322
+ const events = [new TaskUpdatedEvent({
398
323
  taskId,
399
- amount,
400
- kind: "failed"
401
- })]
402
- };
403
- });
404
- }),
405
- completeTask: (taskId) => Effect.gen(function* () {
406
- const now = yield* Clock.currentTimeMillis;
407
- updateState((current) => {
408
- const currentTask = current.tasks.get(taskId);
409
- if (!currentTask) return {
410
- state: current,
411
- events: []
412
- };
413
- if (currentTask.status !== "running") return {
414
- state: current,
415
- events: []
416
- };
417
- const nextTasks = new Map(current.tasks);
418
- if (currentTask.transient) {
419
- const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
420
- for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
421
- const nextColumns = new Map(current.columns);
422
- for (const removedTaskId of removedTaskIds) nextColumns.delete(removedTaskId);
324
+ description: options.description ?? void 0,
325
+ succeeded: options.succeeded ?? void 0,
326
+ failed: options.failed ?? void 0,
327
+ processed: options.succeeded !== void 0 || options.failed !== void 0 ? nextTask.units.processed : void 0,
328
+ total: hasExplicitTotal(options) ? nextTask.units.total : void 0,
329
+ transient: options.transient ?? void 0,
330
+ countDisplay: options.countDisplay ?? void 0
331
+ })];
332
+ if (options.transient !== void 0) for (const candidateId of subtreeTaskIds(current.renderOrder, taskId).slice(1)) {
333
+ const candidate = current.tasks.get(candidateId);
334
+ if (!candidate) continue;
335
+ const nextCandidate = TaskSnapshot({
336
+ ...candidate,
337
+ transient: nextTask.transient
338
+ });
339
+ nextTasks.set(candidateId, nextCandidate);
340
+ events.push(new TaskUpdatedEvent({
341
+ taskId: candidateId,
342
+ transient: nextCandidate.transient
343
+ }));
344
+ }
423
345
  return {
424
346
  state: {
425
347
  tasks: nextTasks,
426
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
427
- columns: nextColumns
348
+ renderOrder: current.renderOrder,
349
+ columns: current.columns
428
350
  },
429
- events: [new TaskCompletedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
351
+ events
430
352
  };
431
- }
432
- const units = currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
433
- succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
434
- failed: currentTask.units.failed,
435
- total: currentTask.units.total
436
- }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
437
- succeeded: currentTask.units.succeeded,
438
- failed: currentTask.units.failed,
439
- total: currentTask.units.processed
440
- }) : currentTask.units;
441
- nextTasks.set(taskId, TaskSnapshot({
442
- ...currentTask,
443
- status: "done",
444
- units,
445
- completedAt: now,
446
- progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
447
- }));
448
- return {
449
- state: {
450
- tasks: nextTasks,
451
- renderOrder: current.renderOrder,
452
- columns: current.columns
453
- },
454
- events: [new TaskCompletedEvent({ taskId })]
455
- };
456
- });
457
- }),
458
- failTask: (taskId) => Effect.gen(function* () {
459
- const now = yield* Clock.currentTimeMillis;
460
- updateState((current) => {
461
- const currentTask = current.tasks.get(taskId);
462
- if (!currentTask) return {
463
- state: current,
464
- events: []
465
- };
466
- if (currentTask.status !== "running") return {
467
- state: current,
468
- events: []
469
- };
470
- const nextTasks = new Map(current.tasks);
471
- if (currentTask.transient) {
472
- const removedTaskIds = subtreeTaskIds(current.renderOrder, taskId);
473
- for (const removedTaskId of removedTaskIds) nextTasks.delete(removedTaskId);
474
- const nextColumns = new Map(current.columns);
475
- for (const removedTaskId of removedTaskIds) nextColumns.delete(removedTaskId);
353
+ }, now);
354
+ }),
355
+ incrementSucceeded: (taskId, amount = 1) => Effect.gen(function* () {
356
+ const now = yield* Clock.currentTimeMillis;
357
+ yield* updateState((current) => {
358
+ const currentTask = current.tasks.get(taskId);
359
+ if (!currentTask) return {
360
+ state: current,
361
+ events: []
362
+ };
363
+ const units = normalizeUnits({
364
+ succeeded: currentTask.units.succeeded + amount,
365
+ failed: currentTask.units.failed,
366
+ total: currentTask.units.total
367
+ });
368
+ const nextTasks = new Map(current.tasks);
369
+ nextTasks.set(taskId, TaskSnapshot({
370
+ ...currentTask,
371
+ units,
372
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
373
+ }));
476
374
  return {
477
375
  state: {
478
376
  tasks: nextTasks,
479
- renderOrder: removeFromRenderOrder(current.renderOrder, taskId),
480
- columns: nextColumns
377
+ renderOrder: current.renderOrder,
378
+ columns: current.columns
481
379
  },
482
- events: [new TaskFailedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
380
+ events: [new TaskAdvancedEvent({
381
+ taskId,
382
+ amount,
383
+ kind: "succeeded"
384
+ })]
483
385
  };
484
- }
485
- nextTasks.set(taskId, TaskSnapshot({
486
- ...currentTask,
487
- status: "failed",
488
- completedAt: now
489
- }));
490
- return {
491
- state: {
492
- tasks: nextTasks,
493
- renderOrder: current.renderOrder,
494
- columns: current.columns
495
- },
496
- events: [new TaskFailedEvent({ taskId })]
497
- };
498
- });
499
- }),
500
- getTask: (taskId) => Effect.sync(() => Option.fromNullable(state.tasks.get(taskId))),
501
- listTasks: Effect.sync(() => Array.from(state.tasks.values())),
502
- setMetadata: (taskId, metadata) => Effect.sync(() => {
503
- updateState((current) => {
504
- const currentTask = current.tasks.get(taskId);
505
- if (!currentTask) return {
506
- state: current,
507
- events: []
508
- };
509
- const nextTasks = new Map(current.tasks);
510
- nextTasks.set(taskId, TaskSnapshot({
511
- ...currentTask,
512
- metadata
513
- }));
514
- return {
515
- state: {
516
- tasks: nextTasks,
517
- renderOrder: current.renderOrder,
518
- columns: current.columns
519
- },
520
- events: []
521
- };
522
- });
523
- }),
524
- getMetadata: (taskId) => Effect.sync(() => {
525
- return state.tasks.get(taskId)?.metadata;
526
- })
386
+ }, now);
387
+ }),
388
+ incrementFailed: (taskId, amount = 1) => Effect.gen(function* () {
389
+ const now = yield* Clock.currentTimeMillis;
390
+ yield* updateState((current) => {
391
+ const currentTask = current.tasks.get(taskId);
392
+ if (!currentTask) return {
393
+ state: current,
394
+ events: []
395
+ };
396
+ const units = normalizeUnits({
397
+ succeeded: currentTask.units.succeeded,
398
+ failed: currentTask.units.failed + amount,
399
+ total: currentTask.units.total
400
+ });
401
+ const nextTasks = new Map(current.tasks);
402
+ nextTasks.set(taskId, TaskSnapshot({
403
+ ...currentTask,
404
+ units,
405
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
406
+ }));
407
+ return {
408
+ state: {
409
+ tasks: nextTasks,
410
+ renderOrder: current.renderOrder,
411
+ columns: current.columns
412
+ },
413
+ events: [new TaskAdvancedEvent({
414
+ taskId,
415
+ amount,
416
+ kind: "failed"
417
+ })]
418
+ };
419
+ }, now);
420
+ }),
421
+ completeTask: (taskId) => Effect.gen(function* () {
422
+ const now = yield* Clock.currentTimeMillis;
423
+ yield* updateState((current) => {
424
+ const currentTask = current.tasks.get(taskId);
425
+ if (!currentTask) return {
426
+ state: current,
427
+ events: []
428
+ };
429
+ if (currentTask.status !== "running") return {
430
+ state: current,
431
+ events: []
432
+ };
433
+ const nextTasks = new Map(current.tasks);
434
+ if (currentTask.transient) {
435
+ const removedSubtree = removeTransientSubtree(current, nextTasks, taskId);
436
+ return {
437
+ state: {
438
+ tasks: nextTasks,
439
+ renderOrder: removedSubtree.renderOrder,
440
+ columns: removedSubtree.columns
441
+ },
442
+ events: [new TaskCompletedEvent({ taskId }), ...removedSubtree.removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
443
+ };
444
+ }
445
+ const units = currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
446
+ succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
447
+ failed: currentTask.units.failed,
448
+ total: currentTask.units.total
449
+ }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
450
+ succeeded: currentTask.units.succeeded,
451
+ failed: currentTask.units.failed,
452
+ total: currentTask.units.processed
453
+ }) : currentTask.units;
454
+ nextTasks.set(taskId, TaskSnapshot({
455
+ ...currentTask,
456
+ status: "done",
457
+ units,
458
+ completedAt: now,
459
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
460
+ }));
461
+ return {
462
+ state: {
463
+ tasks: nextTasks,
464
+ renderOrder: current.renderOrder,
465
+ columns: current.columns
466
+ },
467
+ events: [new TaskCompletedEvent({ taskId })]
468
+ };
469
+ }, now);
470
+ }),
471
+ failTask: (taskId) => Effect.gen(function* () {
472
+ const now = yield* Clock.currentTimeMillis;
473
+ yield* updateState((current) => {
474
+ const currentTask = current.tasks.get(taskId);
475
+ if (!currentTask) return {
476
+ state: current,
477
+ events: []
478
+ };
479
+ if (currentTask.status !== "running") return {
480
+ state: current,
481
+ events: []
482
+ };
483
+ const nextTasks = new Map(current.tasks);
484
+ if (currentTask.transient) {
485
+ const removedSubtree = removeTransientSubtree(current, nextTasks, taskId);
486
+ return {
487
+ state: {
488
+ tasks: nextTasks,
489
+ renderOrder: removedSubtree.renderOrder,
490
+ columns: removedSubtree.columns
491
+ },
492
+ events: [new TaskFailedEvent({ taskId }), ...removedSubtree.removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
493
+ };
494
+ }
495
+ nextTasks.set(taskId, TaskSnapshot({
496
+ ...currentTask,
497
+ status: "failed",
498
+ completedAt: now
499
+ }));
500
+ return {
501
+ state: {
502
+ tasks: nextTasks,
503
+ renderOrder: current.renderOrder,
504
+ columns: current.columns
505
+ },
506
+ events: [new TaskFailedEvent({ taskId })]
507
+ };
508
+ }, now);
509
+ }),
510
+ getTask: (taskId) => Effect.sync(() => Option.fromNullishOr(state.tasks.get(taskId))),
511
+ listTasks: Effect.sync(() => Array.from(state.tasks.values())),
512
+ setMetadata: (taskId, metadata) => Effect.gen(function* () {
513
+ yield* updateState((current) => {
514
+ const currentTask = current.tasks.get(taskId);
515
+ if (!currentTask) return {
516
+ state: current,
517
+ events: []
518
+ };
519
+ const nextTasks = new Map(current.tasks);
520
+ nextTasks.set(taskId, TaskSnapshot({
521
+ ...currentTask,
522
+ metadata
523
+ }));
524
+ return {
525
+ state: {
526
+ tasks: nextTasks,
527
+ renderOrder: current.renderOrder,
528
+ columns: current.columns
529
+ },
530
+ events: []
531
+ };
532
+ }, yield* Clock.currentTimeMillis);
533
+ }),
534
+ getMetadata: (taskId) => Effect.sync(() => {
535
+ return state.tasks.get(taskId)?.metadata;
536
+ })
537
+ },
538
+ publisherLoop
527
539
  };
528
540
  };
541
+ const makeProgressStore = Effect.gen(function* () {
542
+ const runtime = makeProgressStoreRuntime(yield* Queue.sliding(1));
543
+ yield* Effect.forkDetach(runtime.publisherLoop);
544
+ return runtime.store;
545
+ });
546
+ var ProgressStore = class ProgressStore extends Context.Service()("stromseng.dev/effective-progress/ProgressStore") {
547
+ static layer = Layer.effect(ProgressStore, makeProgressStore);
548
+ };
529
549
 
530
550
  //#endregion
531
- //#region src/renderer/hooks/use-now-clock.ts
551
+ //#region src/services/renderer/hooks/use-now-clock.ts
532
552
  const useNowClock = (active, intervalMillis) => {
533
- const [now, setNow] = useState(() => Date.now());
553
+ const [now, updateNow] = useReducer(() => Date.now(), void 0, Date.now);
534
554
  useEffect(() => {
535
555
  if (!active) return;
536
- setNow(Date.now());
556
+ updateNow();
537
557
  const interval = setInterval(() => {
538
- setNow(Date.now());
558
+ updateNow();
539
559
  }, intervalMillis);
540
560
  return () => {
541
561
  clearInterval(interval);
@@ -545,7 +565,7 @@ const useNowClock = (active, intervalMillis) => {
545
565
  };
546
566
 
547
567
  //#endregion
548
- //#region src/renderer/context/now-context.tsx
568
+ //#region src/services/renderer/context/now-context.tsx
549
569
  const NOW_INTERVAL_MILLIS = 1e3;
550
570
  const NowContext = createContext(Date.now());
551
571
  const NowProvider = ({ active, children, nowOverride }) => {
@@ -556,10 +576,10 @@ const NowProvider = ({ active, children, nowOverride }) => {
556
576
  children
557
577
  });
558
578
  };
559
- const useNow = () => useContext(NowContext);
579
+ const useNow = () => use(NowContext);
560
580
 
561
581
  //#endregion
562
- //#region src/renderer/hooks/use-spinner-clock.ts
582
+ //#region src/services/renderer/hooks/use-spinner-clock.ts
563
583
  const normalizeIntervalMillis = (intervalMillis) => Math.max(1, intervalMillis);
564
584
  const getSpinnerTickAtTime = (baseTick, startedAt, now, intervalMillis) => {
565
585
  const elapsedMillis = Math.max(0, now - startedAt);
@@ -590,7 +610,7 @@ const useSpinnerClock = (active, intervalMillis) => {
590
610
  };
591
611
 
592
612
  //#endregion
593
- //#region src/renderer/context/spinner-context.tsx
613
+ //#region src/services/renderer/context/spinner-context.tsx
594
614
  const DEFAULT_SPINNER_INTERVAL_MILLIS = cliSpinners.dots.interval;
595
615
  const SpinnerContext = createContext(0);
596
616
  const SpinnerProvider = ({ active, children, intervalMillis = DEFAULT_SPINNER_INTERVAL_MILLIS, tickOverride }) => {
@@ -601,14 +621,14 @@ const SpinnerProvider = ({ active, children, intervalMillis = DEFAULT_SPINNER_IN
601
621
  children
602
622
  });
603
623
  };
604
- const useSpinnerTick = () => useContext(SpinnerContext);
624
+ const useSpinnerTick = () => use(SpinnerContext);
605
625
 
606
626
  //#endregion
607
- //#region src/renderer/shared/determinate.ts
627
+ //#region src/services/renderer/shared/determinate.ts
608
628
  const isDeterminate$2 = (task) => task.units.total !== void 0;
609
629
 
610
630
  //#endregion
611
- //#region src/renderer/shared/format.ts
631
+ //#region src/services/renderer/shared/format.ts
612
632
  const isDeterminate$1 = (task) => task.units.total !== void 0;
613
633
  const showsUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
614
634
  const formatDurationSeconds = (seconds) => {
@@ -656,7 +676,7 @@ const formatElapsed = (task, now) => {
656
676
  const formatElapsedClock = (task, now) => {
657
677
  return formatClockDurationSeconds(Math.max(0, (task.completedAt ?? now) - task.startedAt) / 1e3);
658
678
  };
659
- const formatEta = (task, now) => {
679
+ const formatEta = (task) => {
660
680
  if (task.status !== "running" || !isDeterminate$1(task)) return "";
661
681
  const { processed, total } = task.units;
662
682
  const remaining = total - processed;
@@ -665,7 +685,7 @@ const formatEta = (task, now) => {
665
685
  if (etaMillis === void 0) return "";
666
686
  return formatClockDurationSeconds(etaMillis / 1e3);
667
687
  };
668
- const formatEtaClock = (task, now) => {
688
+ const formatEtaClock = (task) => {
669
689
  if (task.status !== "running" || !isDeterminate$1(task)) return;
670
690
  const { processed, total } = task.units;
671
691
  const remaining = total - processed;
@@ -674,7 +694,7 @@ const formatEtaClock = (task, now) => {
674
694
  if (etaMillis === void 0) return;
675
695
  return formatClockDurationSeconds(etaMillis / 1e3);
676
696
  };
677
- const formatElapsedEta = (task, now) => `${formatElapsedClock(task, now)}<${formatEtaClock(task, now) ?? "00:00"}`;
697
+ const formatElapsedEta = (task, now) => `${formatElapsedClock(task, now)}<${formatEtaClock(task) ?? "00:00"}`;
678
698
  const formatDeterminateAmountParts = (task) => {
679
699
  if (!isDeterminate$1(task)) return;
680
700
  const totalText = `${task.units.total}`;
@@ -703,7 +723,7 @@ const formatAmount = (task, _tick) => {
703
723
  };
704
724
 
705
725
  //#endregion
706
- //#region src/renderer/shared/text-width.ts
726
+ //#region src/services/renderer/shared/text-width.ts
707
727
  const WIDTH_CACHE_LIMIT = 8192;
708
728
  const widthCache = /* @__PURE__ */ new Map();
709
729
  const textWidth = (text) => {
@@ -716,7 +736,7 @@ const textWidth = (text) => {
716
736
  };
717
737
 
718
738
  //#endregion
719
- //#region src/renderer/columns/amount-column.tsx
739
+ //#region src/services/renderer/columns/amount-column.tsx
720
740
  const hasUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
721
741
  const hasCountedAmount = (task) => isDeterminate$2(task) || hasUnknownTotalCounts(task);
722
742
  const totalTextFor = (task) => task.units.total === void 0 ? "?" : `${task.units.total}`;
@@ -752,7 +772,7 @@ const measureAmountLayout = (rows) => {
752
772
  }, countedWidth)
753
773
  };
754
774
  };
755
- const renderAmount = (task, layout) => {
775
+ const AmountValue = ({ task, layout }) => {
756
776
  if (!hasCountedAmount(task)) return formatAmount(task, 0);
757
777
  const processed = `${task.units.processed}`.padStart(layout.processedWidth, " ");
758
778
  const total = totalTextFor(task).padStart(layout.totalWidth, " ");
@@ -775,16 +795,19 @@ const renderAmount = (task, layout) => {
775
795
  };
776
796
  const AmountCell = ({ task, layout }) => /* @__PURE__ */ jsx(Text, {
777
797
  wrap: "truncate-end",
778
- children: renderAmount(task, layout)
798
+ children: /* @__PURE__ */ jsx(AmountValue, {
799
+ task,
800
+ layout
801
+ })
779
802
  });
780
803
 
781
804
  //#endregion
782
- //#region src/renderer/columns/bar-column.tsx
805
+ //#region src/services/renderer/columns/bar-column.tsx
783
806
  const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
784
807
  const prepareBar = (rows) => {
785
808
  return { hasDeterminateRows: rows.some((row) => row.derived.isDeterminate) };
786
809
  };
787
- const renderProgressBar = (task, width) => {
810
+ const ProgressBarSegments = ({ task, width }) => {
788
811
  if (!isDeterminate$2(task)) return /* @__PURE__ */ jsx(Text, { children: ` `.repeat(Math.max(0, width)) });
789
812
  const displayTotal = Math.max(task.units.total, task.units.succeeded + task.units.failed);
790
813
  const succeededEnd = displayTotal === 0 ? width : Math.round(task.units.succeeded / displayTotal * width);
@@ -812,11 +835,14 @@ const renderProgressBar = (task, width) => {
812
835
  };
813
836
  const BarCell = ({ task, width }) => /* @__PURE__ */ jsx(Text, {
814
837
  wrap: "truncate-end",
815
- children: renderProgressBar(task, width ?? 0)
838
+ children: /* @__PURE__ */ jsx(ProgressBarSegments, {
839
+ task,
840
+ width: width ?? 0
841
+ })
816
842
  });
817
843
 
818
844
  //#endregion
819
- //#region src/renderer/columns/description-column.tsx
845
+ //#region src/services/renderer/columns/description-column.tsx
820
846
  const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
821
847
  const DEFAULT_SPINNER_TYPE = "dots";
822
848
  const isDeterminate = (task) => task.units.total !== void 0;
@@ -894,7 +920,7 @@ const DescriptionCell = ({ cell, width, minTreeWidth, spinnerTick }) => {
894
920
  };
895
921
 
896
922
  //#endregion
897
- //#region src/renderer/columns/elapsed-eta-column.tsx
923
+ //#region src/services/renderer/columns/elapsed-eta-column.tsx
898
924
  const ElapsedEtaCell = ({ task, now }) => {
899
925
  return /* @__PURE__ */ jsx(Text, {
900
926
  wrap: "truncate-end",
@@ -904,7 +930,7 @@ const ElapsedEtaCell = ({ task, now }) => {
904
930
  };
905
931
 
906
932
  //#endregion
907
- //#region src/renderer/columns/elapsed-column.tsx
933
+ //#region src/services/renderer/columns/elapsed-column.tsx
908
934
  const ElapsedCell = ({ task, now }) => {
909
935
  return /* @__PURE__ */ jsx(Text, {
910
936
  wrap: "truncate-end",
@@ -914,9 +940,9 @@ const ElapsedCell = ({ task, now }) => {
914
940
  };
915
941
 
916
942
  //#endregion
917
- //#region src/renderer/columns/eta-column.tsx
918
- const EtaCell = ({ task, now }) => {
919
- const eta = formatEta(task, now);
943
+ //#region src/services/renderer/columns/eta-column.tsx
944
+ const EtaCell = ({ task }) => {
945
+ const eta = formatEta(task);
920
946
  if (eta === "") return null;
921
947
  return /* @__PURE__ */ jsx(Text, {
922
948
  wrap: "truncate-end",
@@ -1006,10 +1032,7 @@ const eta = () => ({
1006
1032
  align: "right",
1007
1033
  flexShrink: 0,
1008
1034
  minWidth: 8,
1009
- render: ({ task }, ctx) => /* @__PURE__ */ jsx(EtaCell, {
1010
- task,
1011
- now: ctx.now
1012
- })
1035
+ render: ({ task }) => /* @__PURE__ */ jsx(EtaCell, { task })
1013
1036
  });
1014
1037
  const defaults = () => [
1015
1038
  description(),
@@ -1023,7 +1046,7 @@ const resolveColumnSizeValue = (value, prepared) => {
1023
1046
  };
1024
1047
 
1025
1048
  //#endregion
1026
- //#region src/renderer/column-resolver.ts
1049
+ //#region src/services/renderer/column-resolver.ts
1027
1050
  const NO_PREPARE = Symbol("no-prepare");
1028
1051
  const getColumnsForRow = (row, columns) => columns.get(row.task.id) ?? defaults();
1029
1052
  const toCellInfo = (row) => row;
@@ -1077,13 +1100,13 @@ const resolveColumns = (rows, columns) => {
1077
1100
  };
1078
1101
 
1079
1102
  //#endregion
1080
- //#region src/renderer/public-api.tsx
1103
+ //#region src/services/renderer/public-api.tsx
1081
1104
  const justifyContentForAlign = (align) => {
1082
1105
  if (align === "right") return "flex-end";
1083
1106
  if (align === "center") return "center";
1084
1107
  return "flex-start";
1085
1108
  };
1086
- const renderNode = (node) => {
1109
+ const RenderedNode = ({ node }) => {
1087
1110
  if (typeof node === "string" || typeof node === "number") return /* @__PURE__ */ jsx(Text, {
1088
1111
  wrap: "truncate-end",
1089
1112
  children: node
@@ -1107,7 +1130,7 @@ const ColumnPosition = ({ position }) => {
1107
1130
  const column = entry?.column;
1108
1131
  const cell = row;
1109
1132
  const output = column?.render(cell, {
1110
- width: hasMeasured ? width : void 0,
1133
+ width: hasMeasured ? width : position.flexBasis,
1111
1134
  now,
1112
1135
  spinnerTick,
1113
1136
  prepared: entry?.prepared
@@ -1115,7 +1138,7 @@ const ColumnPosition = ({ position }) => {
1115
1138
  return /* @__PURE__ */ jsx(Box, {
1116
1139
  height: 1,
1117
1140
  justifyContent: justifyContentForAlign(column?.align),
1118
- children: renderNode(output)
1141
+ children: /* @__PURE__ */ jsx(RenderedNode, { node: output })
1119
1142
  }, row.task.id);
1120
1143
  })
1121
1144
  });
@@ -1131,7 +1154,7 @@ const ProgressRenderer = ({ rows, columns }) => {
1131
1154
  };
1132
1155
 
1133
1156
  //#endregion
1134
- //#region src/renderer/store/render-snapshot.ts
1157
+ //#region src/services/store/render-snapshot.ts
1135
1158
  const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
1136
1159
  const snapshot = store.tasks.get(row.id);
1137
1160
  if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
@@ -1210,7 +1233,7 @@ const toRenderSnapshot = (store, previousSnapshot) => {
1210
1233
  };
1211
1234
 
1212
1235
  //#endregion
1213
- //#region src/renderer/store/use-progress-render-view.ts
1236
+ //#region src/services/renderer/hooks/use-progress-render-view.ts
1214
1237
  const useRenderSnapshot = (storeSnapshot) => {
1215
1238
  const previousSnapshotRef = useRef(void 0);
1216
1239
  const renderSnapshot = useMemo(() => toRenderSnapshot(storeSnapshot, previousSnapshotRef.current), [storeSnapshot]);
@@ -1230,7 +1253,17 @@ const useProgressRenderView = (store) => {
1230
1253
  };
1231
1254
 
1232
1255
  //#endregion
1233
- //#region src/renderer/renderer-service.tsx
1256
+ //#region src/services/stdio.ts
1257
+ const defaultStdioService = {
1258
+ stdout: process.stdout,
1259
+ stderr: process.stderr
1260
+ };
1261
+ var ProgressStdio = class ProgressStdio extends Context.Service()("stromseng.dev/effective-progress/ProgressStdio") {
1262
+ static layer = Layer.succeed(ProgressStdio, defaultStdioService);
1263
+ };
1264
+
1265
+ //#endregion
1266
+ //#region src/services/renderer/renderer.tsx
1234
1267
  const MAX_FPS = 24;
1235
1268
  const ProgressRoot = ({ store }) => {
1236
1269
  const { renderSnapshot, hasRunningTasks, publication } = useProgressRenderView(store);
@@ -1245,57 +1278,43 @@ const ProgressRoot = ({ store }) => {
1245
1278
  })
1246
1279
  });
1247
1280
  };
1248
- const makeRendererv2InkRendererService = () => {
1249
- return { run: (store, stdio) => {
1250
- const proot = /* @__PURE__ */ jsx(ProgressRoot, { store });
1251
- return Effect.sync(() => render(proot, {
1252
- stdout: stdio.stdout,
1253
- stderr: stdio.stderr,
1254
- patchConsole: true,
1255
- exitOnCtrlC: false,
1256
- debug: false,
1257
- maxFps: MAX_FPS
1258
- })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1259
- store.flush();
1260
- instance.rerender(proot);
1261
- yield* Effect.sync(() => {
1262
- instance.unmount();
1263
- });
1264
- })))));
1265
- } };
1266
- };
1267
-
1268
- //#endregion
1269
- //#region src/services/ink-renderer.tsx
1270
- var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
1271
- static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeRendererv2InkRendererService()));
1272
- };
1273
-
1274
- //#endregion
1275
- //#region src/services/stdio.ts
1276
- const defaultStdioService = {
1277
- stdout: process.stdout,
1278
- stderr: process.stderr
1279
- };
1280
- var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effective-progress/ProgressStdio")() {
1281
- static Default = Layer.succeed(ProgressStdio, defaultStdioService);
1281
+ const makeRendererv2InkRendererService = Effect.gen(function* () {
1282
+ const store = yield* ProgressStore;
1283
+ const stdio = yield* ProgressStdio;
1284
+ const proot = /* @__PURE__ */ jsx(ProgressRoot, { store });
1285
+ return { run: Effect.sync(() => render(proot, {
1286
+ stdout: stdio.stdout,
1287
+ stderr: stdio.stderr,
1288
+ patchConsole: true,
1289
+ exitOnCtrlC: false,
1290
+ debug: false,
1291
+ maxFps: MAX_FPS
1292
+ })).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
1293
+ store.flush();
1294
+ instance.rerender(proot);
1295
+ yield* Effect.sync(() => {
1296
+ instance.unmount();
1297
+ });
1298
+ }))))) };
1299
+ });
1300
+ var Renderer = class Renderer extends Context.Service()("stromseng.dev/effective-progress/Renderer") {
1301
+ static layer = Layer.effect(Renderer, makeRendererv2InkRendererService);
1282
1302
  };
1283
1303
 
1284
1304
  //#endregion
1285
1305
  //#region src/services/progress.ts
1286
1306
  /** Builds the scoped implementation used by `ProgressService.task(...)` without auto-providing services. */
1287
1307
  const makeProgressService = Effect.gen(function* () {
1288
- const stdio = yield* ProgressStdio;
1289
- const inkRenderer = yield* InkRenderer;
1290
- const outerConsole = yield* Effect.console;
1291
- const store = makeProgressRenderStore();
1292
- const currentParentRef = yield* FiberRef.make(Option.none());
1308
+ const inkRenderer = yield* Renderer;
1309
+ const store = yield* ProgressStore;
1293
1310
  const scope = yield* Effect.scope;
1294
- const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
1295
- yield* Effect.forkIn(inkRenderer.run(store, stdio), scope);
1311
+ const parentOwner = Symbol();
1312
+ const currentParentId = Effect.map(CurrentParent, (cp) => Option.isSome(cp) && cp.value.owner === parentOwner ? Option.some(cp.value.taskId) : Option.none());
1313
+ const log = (...args) => Effect.log(...args);
1314
+ yield* Effect.forkIn(inkRenderer.run, scope, { startImmediately: true });
1296
1315
  yield* Effect.sleep("0 millis");
1297
1316
  const addTask = (options) => Effect.gen(function* () {
1298
- const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
1317
+ const resolvedParentId = options.parentId === void 0 ? yield* currentParentId : Option.some(options.parentId);
1299
1318
  return yield* store.addTask({
1300
1319
  ...options,
1301
1320
  parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : void 0
@@ -1329,14 +1348,17 @@ const makeProgressService = Effect.gen(function* () {
1329
1348
  else yield* failTask(taskId);
1330
1349
  });
1331
1350
  const scopedTask = dual(2, (effect, options) => Effect.gen(function* () {
1332
- const inheritedParentId = yield* FiberRef.get(currentParentRef);
1351
+ const inheritedParentId = yield* currentParentId;
1333
1352
  const resolvedParentId = options.parentId === void 0 ? inheritedParentId : Option.some(options.parentId);
1334
1353
  const taskId = yield* addTask({
1335
1354
  ...options,
1336
1355
  parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : void 0,
1337
1356
  transient: options.transient
1338
1357
  });
1339
- return yield* Effect.locally(Effect.provideService(effect, Task, taskId), currentParentRef, Option.some(taskId));
1358
+ return yield* Effect.provideService(Effect.provideService(effect, Task, taskId), CurrentParent, Option.some({
1359
+ owner: parentOwner,
1360
+ taskId
1361
+ }));
1340
1362
  }));
1341
1363
  const service = {
1342
1364
  addTask,
@@ -1374,15 +1396,14 @@ const makeProgressService = Effect.gen(function* () {
1374
1396
  };
1375
1397
  return Progress.of(service);
1376
1398
  });
1377
- var Progress = class Progress extends Context.Tag("stromseng.dev/effective-progress/Progress")() {
1378
- static Default = Layer.unwrapEffect(Effect.gen(function* () {
1379
- const stdioOption = yield* Effect.serviceOption(ProgressStdio);
1380
- const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
1381
- let layer = Layer.scoped(Progress, makeProgressService);
1382
- if (Option.isNone(inkRendererOption)) layer = layer.pipe(Layer.provide(InkRenderer.Default));
1383
- if (Option.isNone(stdioOption)) layer = layer.pipe(Layer.provide(ProgressStdio.Default));
1384
- return layer;
1385
- }));
1399
+ /**
1400
+ * Returns a layer that uses an already-provided service for the given tag when available,
1401
+ * or falls back to the supplied default layer otherwise.
1402
+ */
1403
+ const serviceOptionDefaultLayer = (tag, defaultLayer) => Layer.unwrap(Effect.map(Effect.serviceOption(tag), (option) => Option.getOrElse(Option.map(option, (service) => Layer.succeed(tag, service)), () => defaultLayer)));
1404
+ const CurrentParent = Context.Reference("stromseng.dev/effective-progress/CurrentParent", { defaultValue: Option.none });
1405
+ var Progress = class Progress extends Context.Service()("stromseng.dev/effective-progress/Progress") {
1406
+ static layer = Layer.effect(Progress, makeProgressService).pipe(Layer.provide(serviceOptionDefaultLayer(Renderer, Renderer.layer).pipe(Layer.provideMerge(serviceOptionDefaultLayer(ProgressStdio, ProgressStdio.layer)), Layer.provideMerge(ProgressStore.layer))));
1386
1407
  };
1387
1408
 
1388
1409
  //#endregion
@@ -1400,7 +1421,7 @@ const inferTotal = (iterable) => {
1400
1421
  const provideProgress = (effect) => Effect.gen(function* () {
1401
1422
  const existing = yield* Effect.serviceOption(Progress);
1402
1423
  if (Option.isSome(existing)) return yield* Effect.provideService(effect, Progress, existing.value);
1403
- return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
1424
+ return yield* Effect.scoped(Effect.provide(effect, Progress.layer, { local: true }));
1404
1425
  });
1405
1426
  /**
1406
1427
  * Runs an effect inside a task, creating and providing a `Progress` service automatically when one
@@ -1417,7 +1438,7 @@ const task = dual(2, (effectOrCallback, options) => {
1417
1438
  });
1418
1439
  const wrapEffects = (effects, tap) => Array.isArray(effects) ? effects.map(tap) : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
1419
1440
  const countEffects = (effects) => Array.isArray(effects) ? effects.length : Object.keys(effects).length;
1420
- const isCollectAllMode = (mode) => mode === "either" || mode === "validate";
1441
+ const isCollectAllMode = (mode) => mode === "result";
1421
1442
  const allCountDisplay = (mode) => isCollectAllMode(mode) ? "detailed" : "processedOnly";
1422
1443
  const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* () {
1423
1444
  const exit = yield* Effect.exit(effect);
@@ -1425,7 +1446,7 @@ const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* ()
1425
1446
  yield* progress.incrementSucceeded(taskId, 1);
1426
1447
  return exit.value;
1427
1448
  }
1428
- if (Cause.isInterruptedOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
1449
+ if (Cause.hasInterruptsOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
1429
1450
  yield* progress.incrementFailed(taskId, 1);
1430
1451
  return yield* Effect.failCause(exit.cause);
1431
1452
  });
@@ -1445,10 +1466,8 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
1445
1466
  const taskId = handle.id;
1446
1467
  const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => wrapTrackedEffect(progress, taskId, effect)), {
1447
1468
  concurrency: options.concurrency,
1448
- batching: options.batching,
1449
1469
  discard: options.discard,
1450
- mode: options.mode,
1451
- concurrentFinalizers: options.concurrentFinalizers
1470
+ mode: options.mode
1452
1471
  }));
1453
1472
  if (Exit.isSuccess(exit)) yield* progress.completeTask(taskId);
1454
1473
  else if (!isCollectAllMode(options.mode)) yield* progress.failTask(taskId);
@@ -1474,9 +1493,7 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1474
1493
  const taskId = handle.id;
1475
1494
  const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => wrapTrackedEffect(progress, taskId, f(item, index)), {
1476
1495
  concurrency: options.concurrency,
1477
- batching: options.batching,
1478
- discard: options.discard,
1479
- concurrentFinalizers: options.concurrentFinalizers
1496
+ discard: options.discard
1480
1497
  }));
1481
1498
  if (Exit.isSuccess(exit)) yield* progress.completeTask(taskId);
1482
1499
  else yield* progress.failTask(taskId);