effective-progress 0.4.3 → 0.5.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.
@@ -0,0 +1,121 @@
1
+ import { Writable } from "node:stream";
2
+ import { Clock, Context, Effect, Layer, Ref } from "effect";
3
+ import { render, type Instance } from "ink";
4
+ import type { ProgressTerminalService } from "../terminal";
5
+ import type { TaskSnapshot, TaskStore } from "../types";
6
+ import { ProgressApp } from "./app";
7
+ import { toTaskRows } from "./model";
8
+
9
+ const RENDER_INTERVAL_MILLIS = 100;
10
+
11
+ export interface InkRendererService {
12
+ readonly run: (
13
+ storeRef: Ref.Ref<TaskStore>,
14
+ dirtyRef: Ref.Ref<boolean>,
15
+ terminal: ProgressTerminalService,
16
+ isTTY: boolean,
17
+ ) => Effect.Effect<void>;
18
+ }
19
+
20
+ const hasRunningSpinners = (tasks: ReadonlyArray<TaskSnapshot>): boolean =>
21
+ tasks.some(
22
+ (task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits",
23
+ );
24
+
25
+ const createInkWritable = (terminal: ProgressTerminalService): Writable =>
26
+ new Writable({
27
+ write(chunk, _encoding, callback) {
28
+ try {
29
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : `${chunk}`;
30
+ Effect.runSync(terminal.writeStderr(text));
31
+ callback();
32
+ } catch (error) {
33
+ callback(error as Error);
34
+ }
35
+ },
36
+ });
37
+
38
+ const makeDefaultInkRenderer = (): InkRendererService => ({
39
+ run: (storeRef, dirtyRef, terminal, isTTY) =>
40
+ Effect.gen(function* () {
41
+ const output = createInkWritable(terminal);
42
+ let instance: Instance | undefined;
43
+ let tick = 0;
44
+ let rendererActive = false;
45
+
46
+ const renderStore = (
47
+ store: TaskStore,
48
+ now: number,
49
+ terminalColumns: number | undefined,
50
+ ) =>
51
+ Effect.sync(() => {
52
+ const app = (
53
+ <ProgressApp
54
+ rows={toTaskRows(store)}
55
+ now={now}
56
+ tick={tick}
57
+ isTTY={isTTY}
58
+ terminalColumns={terminalColumns}
59
+ />
60
+ );
61
+ if (instance === undefined) {
62
+ instance = render(app, {
63
+ stdout: output as unknown as NodeJS.WriteStream,
64
+ stderr: output as unknown as NodeJS.WriteStream,
65
+ patchConsole: true,
66
+ exitOnCtrlC: false,
67
+ debug: false,
68
+ });
69
+ return;
70
+ }
71
+
72
+ instance.rerender(app);
73
+ });
74
+
75
+ const renderLoop = Effect.gen(function* () {
76
+ rendererActive = true;
77
+
78
+ while (true) {
79
+ const dirty = yield* Ref.getAndSet(dirtyRef, false);
80
+ const store = yield* Ref.get(storeRef);
81
+ const tasks = Array.from(store.tasks.values()).filter(
82
+ (task) => !(task.transient && task.status !== "running"),
83
+ );
84
+ const shouldRender = dirty || hasRunningSpinners(tasks);
85
+
86
+ if (shouldRender) {
87
+ const now = yield* Clock.currentTimeMillis;
88
+ const terminalColumns = isTTY ? yield* terminal.stderrColumns : undefined;
89
+ yield* renderStore(store, now, terminalColumns);
90
+ }
91
+
92
+ tick += 1;
93
+ yield* Effect.sleep(RENDER_INTERVAL_MILLIS);
94
+ }
95
+ }).pipe(
96
+ Effect.ensuring(
97
+ Effect.gen(function* () {
98
+ if (rendererActive) {
99
+ const store = yield* Ref.get(storeRef);
100
+ const now = yield* Clock.currentTimeMillis;
101
+ const terminalColumns = isTTY ? yield* terminal.stderrColumns : undefined;
102
+ yield* renderStore(store, now, terminalColumns);
103
+ }
104
+
105
+ yield* Effect.sync(() => {
106
+ instance?.unmount();
107
+ });
108
+ }),
109
+ ),
110
+ );
111
+
112
+ return yield* renderLoop;
113
+ }),
114
+ });
115
+
116
+ export class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")<
117
+ InkRenderer,
118
+ InkRendererService
119
+ >() {
120
+ static readonly Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
121
+ }
@@ -0,0 +1,53 @@
1
+ import { Box } from "ink";
2
+ import { DEFAULT_BAR_WIDTH, type SharedColumnWidths } from "./layout";
3
+ import type { TaskRowModel } from "./types";
4
+ import {
5
+ AmountColumn,
6
+ BarColumn,
7
+ DescriptionColumn,
8
+ ElapsedColumn,
9
+ EtaColumn,
10
+ } from "./columns";
11
+
12
+ export interface TaskRowProps {
13
+ readonly row: TaskRowModel;
14
+ readonly now: number;
15
+ readonly tick: number;
16
+ readonly isTTY: boolean;
17
+ readonly widths: SharedColumnWidths;
18
+ }
19
+
20
+ export const TaskRow = ({ row, now, tick, isTTY, widths }: TaskRowProps) => {
21
+ const props = {
22
+ task: row.task,
23
+ tree: row.tree,
24
+ now,
25
+ tick,
26
+ isTTY,
27
+ showTree: widths.showTree,
28
+ } as const;
29
+
30
+ return (
31
+ <Box flexDirection="row" minWidth={widths.row}>
32
+ <Box width={widths.description} flexShrink={1} marginRight={1}>
33
+ <DescriptionColumn {...props} />
34
+ </Box>
35
+ {widths.bar > 0 ? (
36
+ <Box width={widths.bar} flexShrink={0} marginRight={1}>
37
+ <BarColumn {...props} width={Math.max(1, Math.min(widths.bar, DEFAULT_BAR_WIDTH))} />
38
+ </Box>
39
+ ) : null}
40
+ <Box width={widths.amount} flexShrink={0} marginRight={1}>
41
+ <AmountColumn {...props} />
42
+ </Box>
43
+ <Box width={widths.elapsed} flexShrink={0} marginRight={1}>
44
+ <ElapsedColumn {...props} />
45
+ </Box>
46
+ {widths.eta > 0 ? (
47
+ <Box width={widths.eta} flexShrink={0}>
48
+ <EtaColumn {...props} />
49
+ </Box>
50
+ ) : null}
51
+ </Box>
52
+ );
53
+ };
@@ -1,10 +1,4 @@
1
- import type { TaskSnapshot } from "../types";
2
- import type { TaskTreeInfo } from "./types";
3
-
4
- export interface OrderedTreeTask {
5
- readonly snapshot: TaskSnapshot;
6
- readonly depth: number;
7
- }
1
+ import type { OrderedTask, TaskTreeInfo } from "./types";
8
2
 
9
3
  const treeAncestorPrefix = (tree: TaskTreeInfo): string =>
10
4
  tree.ancestorHasNextSibling
@@ -22,8 +16,8 @@ export const renderTreePrefix = (tree: TaskTreeInfo): string => {
22
16
  };
23
17
 
24
18
  export const computeTreeInfo = (
25
- ordered: ReadonlyArray<OrderedTreeTask>,
26
- ): ReadonlyArray<OrderedTreeTask & { readonly tree: TaskTreeInfo }> => {
19
+ ordered: ReadonlyArray<OrderedTask>,
20
+ ): ReadonlyArray<OrderedTask & { readonly tree: TaskTreeInfo }> => {
27
21
  const hasNextSiblingByIndex: Array<boolean> = Array.from({ length: ordered.length }, () => false);
28
22
 
29
23
  for (let i = 0; i < ordered.length; i++) {
@@ -0,0 +1,18 @@
1
+ import type { TaskSnapshot } from "../types";
2
+
3
+ export interface TaskTreeInfo {
4
+ readonly depth: number;
5
+ readonly hasNextSibling: boolean;
6
+ readonly hasChildren: boolean;
7
+ readonly ancestorHasNextSibling: ReadonlyArray<boolean>;
8
+ }
9
+
10
+ export interface OrderedTask {
11
+ readonly snapshot: TaskSnapshot;
12
+ readonly depth: number;
13
+ }
14
+
15
+ export interface TaskRowModel {
16
+ readonly task: TaskSnapshot;
17
+ readonly tree: TaskTreeInfo;
18
+ }
package/src/runtime.ts CHANGED
@@ -1,47 +1,22 @@
1
1
  import { Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
2
2
  import { dual } from "effect/Function";
3
- import { mergeWith } from "es-toolkit/object";
4
- import type { PartialDeep } from "type-fest";
5
- import { type BufferedConsoleCall, makeConsoleBridge } from "./console";
6
- import { Columns, FrameRenderer } from "./renderer";
3
+ import { InkRenderer } from "./ink-renderer";
7
4
  import { ProgressTerminal } from "./terminal";
8
5
  import type {
9
6
  AddTaskOptions,
10
7
  ProgressService,
11
8
  RenderRow,
12
- RendererConfigShape,
13
9
  TaskStore,
14
10
  UpdateTaskOptions,
15
11
  } from "./types";
16
12
  import {
17
- decodeProgressBarConfigSync,
18
- decodeRendererConfigSync,
19
- defaultProgressBarConfig,
20
- defaultRendererConfig,
21
13
  DeterminateTaskUnits,
22
14
  IndeterminateTaskUnits,
23
- ProgressBarConfig,
24
- RendererConfig,
25
15
  Task,
26
16
  TaskId,
27
17
  TaskSnapshot,
28
18
  } from "./types";
29
19
 
30
- const mergeConfig = <T extends Record<PropertyKey, any>>(
31
- base: T,
32
- override: PartialDeep<T> | undefined,
33
- ): T =>
34
- mergeWith(
35
- structuredClone(base),
36
- (override ?? {}) as Record<PropertyKey, any>,
37
- (_targetValue, sourceValue) => {
38
- if (Array.isArray(sourceValue)) {
39
- return sourceValue;
40
- }
41
- return undefined;
42
- },
43
- ) as T;
44
-
45
20
  const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
46
21
  const currentUnits = snapshot.units;
47
22
  const units = (() => {
@@ -81,7 +56,6 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
81
56
  status: snapshot.status,
82
57
  transient: options.transient ?? snapshot.transient,
83
58
  units,
84
- config: snapshot.config,
85
59
  startedAt: snapshot.startedAt,
86
60
  completedAt: snapshot.completedAt,
87
61
  });
@@ -95,7 +69,6 @@ const withTransient = (snapshot: TaskSnapshot, transient: boolean): TaskSnapshot
95
69
  status: snapshot.status,
96
70
  transient,
97
71
  units: snapshot.units,
98
- config: snapshot.config,
99
72
  startedAt: snapshot.startedAt,
100
73
  completedAt: snapshot.completedAt,
101
74
  });
@@ -129,33 +102,9 @@ const removeFromRenderOrder = (
129
102
  return next;
130
103
  };
131
104
 
132
- const withDefaultColumns = (rendererConfig: RendererConfigShape): RendererConfigShape => ({
133
- ...rendererConfig,
134
- columns: rendererConfig.columns.length > 0 ? rendererConfig.columns : Columns.defaults(),
135
- });
136
-
137
105
  const makeProgressService = Effect.gen(function* () {
138
- const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
139
- const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
140
-
141
- const rendererConfig = withDefaultColumns(
142
- decodeRendererConfigSync(
143
- mergeConfig(
144
- defaultRendererConfig,
145
- Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
146
- ),
147
- ),
148
- );
149
-
150
- const progressBarConfig = decodeProgressBarConfigSync(
151
- mergeConfig(
152
- defaultProgressBarConfig,
153
- Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
154
- ),
155
- );
156
-
157
106
  const terminal = yield* ProgressTerminal;
158
- const frameRenderer = yield* FrameRenderer;
107
+ const inkRenderer = yield* InkRenderer;
159
108
  const outerConsole = yield* Effect.console;
160
109
  const isTTY = yield* terminal.isTTY;
161
110
 
@@ -164,30 +113,15 @@ const makeProgressService = Effect.gen(function* () {
164
113
  tasks: new Map<TaskId, TaskSnapshot>(),
165
114
  renderOrder: [],
166
115
  });
167
- const pendingLogsRef = yield* Ref.make<ReadonlyArray<BufferedConsoleCall>>([]);
168
116
  const dirtyRef = yield* Ref.make(true);
169
117
  const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
170
118
  const scope = yield* Effect.scope;
171
119
 
172
120
  const markDirty = Ref.set(dirtyRef, true);
173
- const { replayLogs, progressConsole, log } = makeConsoleBridge(
174
- outerConsole,
175
- pendingLogsRef,
176
- markDirty,
177
- );
121
+ const log = (...args: ReadonlyArray<unknown>) =>
122
+ args.length === 0 ? Effect.void : outerConsole.log(...args);
178
123
 
179
- yield* Effect.forkIn(
180
- frameRenderer.run(
181
- storeRef,
182
- pendingLogsRef,
183
- replayLogs,
184
- dirtyRef,
185
- terminal,
186
- isTTY,
187
- rendererConfig,
188
- ),
189
- scope,
190
- );
124
+ yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef, terminal, isTTY), scope);
191
125
  // Let the renderer fiber start so queued logs are reliably flushed on scope teardown.
192
126
  yield* Effect.sleep("0 millis");
193
127
 
@@ -206,10 +140,6 @@ const makeProgressService = Effect.gen(function* () {
206
140
  const parentSnapshot = Option.isSome(resolvedParentId)
207
141
  ? store.tasks.get(resolvedParentId.value)
208
142
  : undefined;
209
- const inheritedProgressBarConfig = parentSnapshot?.config ?? progressBarConfig;
210
- const resolvedProgressBarConfig = decodeProgressBarConfigSync(
211
- mergeConfig(inheritedProgressBarConfig, options.progressbar),
212
- );
213
143
 
214
144
  const now = yield* Clock.currentTimeMillis;
215
145
  const parentIdValue = Option.getOrNull(resolvedParentId);
@@ -220,7 +150,6 @@ const makeProgressService = Effect.gen(function* () {
220
150
  status: "running",
221
151
  transient: parentSnapshot?.transient ?? options.transient ?? false,
222
152
  units,
223
- config: resolvedProgressBarConfig,
224
153
  startedAt: now,
225
154
  completedAt: null,
226
155
  });
@@ -296,7 +225,6 @@ const makeProgressService = Effect.gen(function* () {
296
225
  status: snapshot.status,
297
226
  transient: snapshot.transient,
298
227
  units,
299
- config: snapshot.config,
300
228
  startedAt: snapshot.startedAt,
301
229
  completedAt: snapshot.completedAt,
302
230
  }),
@@ -336,7 +264,6 @@ const makeProgressService = Effect.gen(function* () {
336
264
  total: snapshot.units.total,
337
265
  })
338
266
  : snapshot.units,
339
- config: snapshot.config,
340
267
  startedAt: snapshot.startedAt,
341
268
  completedAt: now,
342
269
  }),
@@ -371,7 +298,6 @@ const makeProgressService = Effect.gen(function* () {
371
298
  status: "failed",
372
299
  transient: snapshot.transient,
373
300
  units: snapshot.units,
374
- config: snapshot.config,
375
301
  startedAt: snapshot.startedAt,
376
302
  completedAt: now,
377
303
  }),
@@ -401,7 +327,7 @@ const makeProgressService = Effect.gen(function* () {
401
327
  });
402
328
 
403
329
  return yield* Effect.locally(
404
- Effect.withConsole(Effect.provideService(effect, Task, taskId), progressConsole),
330
+ Effect.provideService(effect, Task, taskId),
405
331
  currentParentRef,
406
332
  Option.some(taskId),
407
333
  );
@@ -454,11 +380,11 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
454
380
  static readonly Default = Layer.unwrapEffect(
455
381
  Effect.gen(function* () {
456
382
  const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
457
- const frameRendererOption = yield* Effect.serviceOption(FrameRenderer);
383
+ const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
458
384
  let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
459
385
 
460
- if (Option.isNone(frameRendererOption)) {
461
- layer = layer.pipe(Layer.provide(FrameRenderer.Default));
386
+ if (Option.isNone(inkRendererOption)) {
387
+ layer = layer.pipe(Layer.provide(InkRenderer.Default));
462
388
  }
463
389
  if (Option.isNone(terminalOption)) {
464
390
  layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
package/src/types.ts CHANGED
@@ -1,72 +1,4 @@
1
1
  import { Brand, Context, Effect, Option, Schema } from "effect";
2
- import type { PartialDeep } from "type-fest";
3
- import type { ProgressColumn } from "./renderer";
4
-
5
- export const RendererConfigSchema = Schema.Struct({
6
- disableUserInput: Schema.Boolean,
7
- renderIntervalMillis: Schema.Number,
8
- nonTtyUpdateStep: Schema.Number,
9
- width: Schema.Union(Schema.Number, Schema.Literal("fullwidth")),
10
- columnGap: Schema.Number,
11
- columns: Schema.Array(Schema.Unknown),
12
- });
13
- export type RendererConfigShape = {
14
- readonly disableUserInput: boolean;
15
- readonly renderIntervalMillis: number;
16
- readonly nonTtyUpdateStep: number;
17
- readonly width: number | "fullwidth";
18
- readonly columnGap: number;
19
- readonly columns: ReadonlyArray<ProgressColumn | string>;
20
- };
21
- const decodeRendererConfigSchemaSync = Schema.decodeUnknownSync(RendererConfigSchema);
22
- export const decodeRendererConfigSync = (input: unknown): RendererConfigShape => {
23
- if (typeof input === "object" && input !== null && "determinateTaskLayout" in input) {
24
- throw new Error("determinateTaskLayout has been removed. Use columns instead.");
25
- }
26
- if (typeof input === "object" && input !== null && "maxTaskWidth" in input) {
27
- throw new Error("maxTaskWidth has been removed. Use width on RendererConfig instead.");
28
- }
29
-
30
- return decodeRendererConfigSchemaSync(input) as RendererConfigShape;
31
- };
32
-
33
- export const ProgressBarConfigSchema = Schema.Struct({
34
- spinnerFrames: Schema.NonEmptyArray(Schema.String),
35
- barWidth: Schema.Number,
36
- fillChar: Schema.String,
37
- emptyChar: Schema.String,
38
- leftBracket: Schema.String,
39
- rightBracket: Schema.String,
40
- });
41
- export type ProgressBarConfigShape = typeof ProgressBarConfigSchema.Type;
42
- export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarConfigSchema);
43
-
44
- export const defaultRendererConfig: RendererConfigShape = {
45
- disableUserInput: true,
46
- renderIntervalMillis: 100, // 10 FPS
47
- nonTtyUpdateStep: 5,
48
- width: 120,
49
- columnGap: 1,
50
- columns: [],
51
- };
52
-
53
- export const defaultProgressBarConfig: ProgressBarConfigShape = {
54
- spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
55
- barWidth: 40,
56
- fillChar: "━",
57
- emptyChar: "─",
58
- leftBracket: "",
59
- rightBracket: "",
60
- };
61
-
62
- export class RendererConfig extends Context.Tag("stromseng.dev/effective-progress/RendererConfig")<
63
- RendererConfig,
64
- PartialDeep<RendererConfigShape>
65
- >() {}
66
-
67
- export class ProgressBarConfig extends Context.Tag(
68
- "stromseng.dev/effective-progress/ProgressBarConfig",
69
- )<ProgressBarConfig, PartialDeep<ProgressBarConfigShape>>() {}
70
2
 
71
3
  const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
72
4
 
@@ -82,7 +14,6 @@ export interface AddTaskOptions {
82
14
  readonly total?: number;
83
15
  readonly transient?: boolean;
84
16
  readonly parentId?: TaskId;
85
- readonly progressbar?: PartialDeep<ProgressBarConfigShape>;
86
17
  }
87
18
 
88
19
  export interface UpdateTaskOptions {
@@ -120,7 +51,6 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
120
51
  status: TaskStatusSchema,
121
52
  transient: Schema.Boolean,
122
53
  units: TaskUnitsSchema,
123
- config: ProgressBarConfigSchema,
124
54
  startedAt: Schema.Number,
125
55
  completedAt: Schema.NullOr(Schema.Number),
126
56
  }) {}
package/src/console.ts DELETED
@@ -1,177 +0,0 @@
1
- import { Console, Effect, Ref } from "effect";
2
-
3
- export type BufferedConsoleMethod =
4
- | "assert"
5
- | "debug"
6
- | "dir"
7
- | "dirxml"
8
- | "error"
9
- | "group"
10
- | "groupCollapsed"
11
- | "groupEnd"
12
- | "info"
13
- | "log"
14
- | "table"
15
- | "trace"
16
- | "warn";
17
-
18
- export interface BufferedConsoleCall {
19
- readonly method: BufferedConsoleMethod;
20
- readonly args: ReadonlyArray<unknown>;
21
- readonly unsafe: boolean;
22
- }
23
-
24
- export interface ConsoleBridge {
25
- readonly progressConsole: Console.Console;
26
- readonly appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>;
27
- readonly replayLogs: (
28
- logs: ReadonlyArray<BufferedConsoleCall>,
29
- ) => Effect.Effect<void, never, never>;
30
- readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void, never, never>;
31
- }
32
-
33
- const replayBufferedConsoleCall = (
34
- outerConsole: Console.Console,
35
- call: BufferedConsoleCall,
36
- ): Effect.Effect<void, never, never> => {
37
- type DirOptions = Parameters<Console.Console["dir"]>[1];
38
- type GroupOptions = Parameters<Console.Console["group"]>[0];
39
-
40
- switch (call.method) {
41
- case "assert": {
42
- const [condition, ...rest] = call.args;
43
- return outerConsole.assert(condition as boolean, ...rest);
44
- }
45
- case "debug":
46
- return outerConsole.debug(...call.args);
47
- case "dir":
48
- return outerConsole.dir(call.args[0], call.args[1] as DirOptions);
49
- case "dirxml":
50
- return outerConsole.dirxml(...call.args);
51
- case "error":
52
- return outerConsole.error(...call.args);
53
- case "group":
54
- return outerConsole.group(call.args[0] as GroupOptions);
55
- case "groupCollapsed":
56
- return outerConsole.group(call.args[0] as GroupOptions);
57
- case "groupEnd":
58
- return outerConsole.groupEnd;
59
- case "info":
60
- return outerConsole.info(...call.args);
61
- case "log":
62
- return outerConsole.log(...call.args);
63
- case "table":
64
- return outerConsole.table(call.args[0], call.args[1] as ReadonlyArray<string> | undefined);
65
- case "trace":
66
- return outerConsole.trace(...call.args);
67
- case "warn":
68
- return outerConsole.warn(...call.args);
69
- }
70
- };
71
-
72
- export const makeProgressConsole = (
73
- appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>,
74
- ): Console.Console => {
75
- const log = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) =>
76
- appendLog({ method, args, unsafe: false });
77
- const unsafeLog = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) => {
78
- Effect.runSync(appendLog({ method, args, unsafe: true }));
79
- };
80
-
81
- return Console.Console.of({
82
- [Console.TypeId]: Console.TypeId,
83
- assert: (condition, ...args) => log("assert", condition, ...args),
84
- clear: Effect.void,
85
- count: (_label) => Effect.void,
86
- countReset: (_label) => Effect.void,
87
- debug: (...args) => log("debug", ...args),
88
- dir: (item, options) => log("dir", item, options),
89
- dirxml: (...args) => log("dirxml", ...args),
90
- error: (...args) => log("error", ...args),
91
- group: (...args) => log("group", ...args),
92
- groupEnd: log("groupEnd"),
93
- info: (...args) => log("info", ...args),
94
- log: (...args) => log("log", ...args),
95
- table: (tabularData, properties) => log("table", tabularData, properties),
96
- time: (_label) => Effect.void,
97
- timeEnd: (_label) => Effect.void,
98
- timeLog: (_label, ...args) => log("info", ...args),
99
- trace: (...args) => log("trace", ...args),
100
- warn: (...args) => log("warn", ...args),
101
- unsafe: {
102
- assert(condition, ...args) {
103
- unsafeLog("assert", condition, ...args);
104
- },
105
- clear() {},
106
- count(_label) {},
107
- countReset(_label) {},
108
- debug(...args) {
109
- unsafeLog("debug", ...args);
110
- },
111
- dir(item, options) {
112
- unsafeLog("dir", item, options);
113
- },
114
- dirxml(...args) {
115
- unsafeLog("dirxml", ...args);
116
- },
117
- error(...args) {
118
- unsafeLog("error", ...args);
119
- },
120
- group(...args) {
121
- unsafeLog("group", ...args);
122
- },
123
- groupCollapsed(...args) {
124
- unsafeLog("groupCollapsed", ...args);
125
- },
126
- groupEnd() {
127
- unsafeLog("groupEnd");
128
- },
129
- info(...args) {
130
- unsafeLog("info", ...args);
131
- },
132
- log(...args) {
133
- unsafeLog("log", ...args);
134
- },
135
- table(tabularData, properties) {
136
- unsafeLog("table", tabularData, properties);
137
- },
138
- time(_label) {},
139
- timeEnd(_label) {},
140
- timeLog(_label, ...args) {
141
- unsafeLog("info", ...args);
142
- },
143
- trace(...args) {
144
- unsafeLog("trace", ...args);
145
- },
146
- warn(...args) {
147
- unsafeLog("warn", ...args);
148
- },
149
- },
150
- });
151
- };
152
-
153
- export const makeConsoleBridge = (
154
- outerConsole: Console.Console,
155
- pendingLogsRef: Ref.Ref<ReadonlyArray<BufferedConsoleCall>>,
156
- markDirty: Effect.Effect<void, never, never>,
157
- ): ConsoleBridge => {
158
- const appendLog = (call: BufferedConsoleCall) =>
159
- call.args.length === 0
160
- ? Effect.void
161
- : Ref.update(pendingLogsRef, (logs) => [...logs, call]).pipe(Effect.zipRight(markDirty));
162
-
163
- const replayLogs = (logs: ReadonlyArray<BufferedConsoleCall>) =>
164
- Effect.forEach(logs, (call) => replayBufferedConsoleCall(outerConsole, call), {
165
- discard: true,
166
- });
167
-
168
- const log = (...args: ReadonlyArray<unknown>) =>
169
- args.length === 0 ? Effect.void : appendLog({ method: "log", args, unsafe: false });
170
-
171
- return {
172
- progressConsole: makeProgressConsole(appendLog),
173
- appendLog,
174
- replayLogs,
175
- log,
176
- };
177
- };