task_progress 0.0.1

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.
Files changed (107) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +29 -0
  3. data/LICENSE +21 -0
  4. data/MANUAL.md +851 -0
  5. data/README.md +158 -0
  6. data/RECIPES.md +413 -0
  7. data/config/importmap.rb +17 -0
  8. data/exe/task_progress +51 -0
  9. data/js/local_tasks.d.ts +44 -0
  10. data/js/local_tasks.js +229 -0
  11. data/js/package.json +31 -0
  12. data/js/paths.d.ts +5 -0
  13. data/js/paths.js +7 -0
  14. data/js/react_task_tree.d.ts +49 -0
  15. data/js/react_task_tree.js +123 -0
  16. data/js/render_queue.d.ts +24 -0
  17. data/js/render_queue.js +79 -0
  18. data/js/row_view.d.ts +25 -0
  19. data/js/row_view.js +69 -0
  20. data/js/stimulus_controller.d.ts +10 -0
  21. data/js/stimulus_controller.js +137 -0
  22. data/js/task_progress_clock.d.ts +2 -0
  23. data/js/task_progress_clock.js +33 -0
  24. data/js/task_tree.css +51 -0
  25. data/js/task_tree.d.ts +111 -0
  26. data/js/task_tree.js +444 -0
  27. data/lib/task_progress/assertions.rb +49 -0
  28. data/lib/task_progress/batch.rb +23 -0
  29. data/lib/task_progress/branch.rb +74 -0
  30. data/lib/task_progress/buffered_output.rb +63 -0
  31. data/lib/task_progress/cancellations_app.rb +76 -0
  32. data/lib/task_progress/event.rb +46 -0
  33. data/lib/task_progress/execution_log_output.rb +183 -0
  34. data/lib/task_progress/fold.rb +18 -0
  35. data/lib/task_progress/html_renderer.rb +153 -0
  36. data/lib/task_progress/job_serializer.rb +92 -0
  37. data/lib/task_progress/journal.rb +35 -0
  38. data/lib/task_progress/log.rb +263 -0
  39. data/lib/task_progress/memory_cache.rb +31 -0
  40. data/lib/task_progress/memory_stream_store.rb +142 -0
  41. data/lib/task_progress/mount.rb +448 -0
  42. data/lib/task_progress/ndjson_output.rb +24 -0
  43. data/lib/task_progress/node.rb +52 -0
  44. data/lib/task_progress/poll.rb +87 -0
  45. data/lib/task_progress/publishing_output.rb +30 -0
  46. data/lib/task_progress/ractor_output.rb +23 -0
  47. data/lib/task_progress/rails_defaults.rb +46 -0
  48. data/lib/task_progress/railtie.rb +91 -0
  49. data/lib/task_progress/recording_output.rb +51 -0
  50. data/lib/task_progress/redis_stream_store.rb +131 -0
  51. data/lib/task_progress/squasher.rb +59 -0
  52. data/lib/task_progress/sse.rb +114 -0
  53. data/lib/task_progress/state.rb +319 -0
  54. data/lib/task_progress/stream_output.rb +222 -0
  55. data/lib/task_progress/stream_reader.rb +76 -0
  56. data/lib/task_progress/summary_output.rb +37 -0
  57. data/lib/task_progress/terminal_output.rb +241 -0
  58. data/lib/task_progress/token.rb +58 -0
  59. data/lib/task_progress/tracked_io.rb +68 -0
  60. data/lib/task_progress/transaction.rb +41 -0
  61. data/lib/task_progress/trees_app.rb +66 -0
  62. data/lib/task_progress/turbo_helper.rb +18 -0
  63. data/lib/task_progress/turbo_sse.rb +103 -0
  64. data/lib/task_progress/turbo_stream_output.rb +61 -0
  65. data/lib/task_progress/version.rb +7 -0
  66. data/lib/task_progress/wire.rb +211 -0
  67. data/lib/task_progress.rb +1036 -0
  68. data/sig/generated/task_progress/assertions.rbs +36 -0
  69. data/sig/generated/task_progress/batch.rbs +33 -0
  70. data/sig/generated/task_progress/branch.rbs +47 -0
  71. data/sig/generated/task_progress/buffered_output.rbs +38 -0
  72. data/sig/generated/task_progress/cancellations_app.rbs +41 -0
  73. data/sig/generated/task_progress/event.rbs +20 -0
  74. data/sig/generated/task_progress/execution_log_output.rbs +93 -0
  75. data/sig/generated/task_progress/fold.rbs +25 -0
  76. data/sig/generated/task_progress/html_renderer.rbs +71 -0
  77. data/sig/generated/task_progress/journal.rbs +25 -0
  78. data/sig/generated/task_progress/log.rbs +140 -0
  79. data/sig/generated/task_progress/memory_cache.rbs +28 -0
  80. data/sig/generated/task_progress/memory_stream_store.rbs +83 -0
  81. data/sig/generated/task_progress/mount.rbs +200 -0
  82. data/sig/generated/task_progress/ndjson_output.rbs +18 -0
  83. data/sig/generated/task_progress/node.rbs +66 -0
  84. data/sig/generated/task_progress/poll.rbs +50 -0
  85. data/sig/generated/task_progress/publishing_output.rbs +24 -0
  86. data/sig/generated/task_progress/ractor_output.rbs +17 -0
  87. data/sig/generated/task_progress/rails_defaults.rbs +33 -0
  88. data/sig/generated/task_progress/recording_output.rbs +41 -0
  89. data/sig/generated/task_progress/redis_stream_store.rbs +75 -0
  90. data/sig/generated/task_progress/squasher.rbs +20 -0
  91. data/sig/generated/task_progress/sse.rbs +73 -0
  92. data/sig/generated/task_progress/state.rbs +165 -0
  93. data/sig/generated/task_progress/stream_output.rbs +142 -0
  94. data/sig/generated/task_progress/stream_reader.rbs +46 -0
  95. data/sig/generated/task_progress/summary_output.rbs +19 -0
  96. data/sig/generated/task_progress/terminal_output.rbs +117 -0
  97. data/sig/generated/task_progress/token.rbs +41 -0
  98. data/sig/generated/task_progress/tracked_io.rbs +48 -0
  99. data/sig/generated/task_progress/transaction.rbs +33 -0
  100. data/sig/generated/task_progress/trees_app.rbs +40 -0
  101. data/sig/generated/task_progress/turbo_helper.rbs +14 -0
  102. data/sig/generated/task_progress/turbo_sse.rbs +69 -0
  103. data/sig/generated/task_progress/turbo_stream_output.rbs +51 -0
  104. data/sig/generated/task_progress/version.rbs +6 -0
  105. data/sig/generated/task_progress/wire.rbs +102 -0
  106. data/sig/generated/task_progress.rbs +553 -0
  107. metadata +178 -0
@@ -0,0 +1,553 @@
1
+ # Generated from lib/task_progress.rb with RBS::Inline
2
+
3
+ # Hierarchical progress for task trees. A TaskProgress is a handle onto one node of a tree; the
4
+ # tree itself is a Log of change events, applied and numbered under one lock, which outputs
5
+ # receive in order - to draw a terminal, write to a pipe, stream to a browser, or feed a Mount
6
+ # that grafts the tree under a node of a tree in another process.
7
+ #
8
+ # TaskProgress.cli("Import") do |root|
9
+ # root.subtask("Download", unit: "bytes") { |t| t.track_io(io) ... }
10
+ # root.subtask("Rows", total: rows.size) { |t| rows.each { import(it); t.tick } }
11
+ # end
12
+ class TaskProgress
13
+ # A task's place in the tree: its ancestors' ids, then its own - `[]` is the root
14
+ type path = Array[String]
15
+
16
+ # Anything that takes the batches a log commits, in order - a lambda will do
17
+ interface _Output
18
+ def call: (Batch batch, Log log) -> void
19
+ end
20
+
21
+ type output = Object & _Output
22
+
23
+ PARTS: String
24
+
25
+ # Ractors on Ruby 3.4 corrupt the VM's table of call sites: a `super` from Ruby into a C
26
+ # constructor (Data#initialize, say) while Ractors run brings the process down, sometimes
27
+ # minutes later and in unrelated code. Ruby 4.0 has it fixed, and the Ractor::Port API with it
28
+ RACTORS: bool
29
+
30
+ # Loads every part now rather than on first use - before forking workers, say, so they share
31
+ # the loaded code. TrackedIO is left out when the progressive_io gem is not there, and
32
+ # JobSerializer when ActiveJob is not
33
+ # : () -> singleton(TaskProgress)
34
+ def self.eager_load!: () -> singleton(TaskProgress)
35
+
36
+ OPTIONAL: Array[Symbol]
37
+
38
+ # Raised at whoever works on a task that was called off, so the work stops at its next step.
39
+ # An Exception rather than a StandardError, like Interrupt: a `rescue => e` around one step
40
+ # should not swallow it and carry on with the next
41
+ class Canceled < Exception
42
+ # The task that was called off
43
+ attr_reader task: TaskProgress
44
+
45
+ # For the task that was called off, and what it is called
46
+ # : (TaskProgress, ?String) -> void
47
+ def initialize: (TaskProgress, ?String) -> void
48
+ end
49
+
50
+ # The log the task's tree lives in
51
+ attr_reader log: Log
52
+
53
+ # The task's place in the tree - `[]` for the root
54
+ attr_reader path: path
55
+
56
+ ROOT: path
57
+
58
+ # A root reporting wherever suits a command-line program: a parent process, when it handed
59
+ # one down in TASK_PROGRESS_FD; a redrawn tree on a terminal; an ExecutionLog on CI or when
60
+ # piped. With a block, it runs the block as the root's work and returns its value
61
+ # : (?String, ?io: IO | StringIO, ?quiet: bool, **untyped) -> TaskProgress
62
+ # : [T] (?String, ?io: IO | StringIO, ?quiet: bool, **untyped) { (TaskProgress) -> T } -> T?
63
+ def self.cli: (?String, ?io: IO | StringIO, ?quiet: bool, **untyped) -> TaskProgress
64
+ | [T] (?String, ?io: IO | StringIO, ?quiet: bool, **untyped) { (TaskProgress) -> T } -> T?
65
+
66
+ # The output `cli` and `bar` pick for where the program runs, or nil when `quiet`
67
+ # : (io: IO | StringIO, quiet: bool) -> output?
68
+ def self.cli_output: (io: IO | StringIO, quiet: bool) -> output?
69
+
70
+ # A single bar, the way progress bars always worked: no block, and it finishes by itself once
71
+ # its count reaches `total`. Without a total it counts and spins. The last frame is drawn at
72
+ # exit, finished or not
73
+ #
74
+ # bar = TaskProgress.bar("Importing", total: rows.size)
75
+ # rows.each { import(it) && bar.tick }
76
+ # : (?String, ?total: Integer?, ?io: IO | StringIO, ?quiet: bool, **untyped) -> TaskProgress
77
+ def self.bar: (?String, ?total: Integer?, ?io: IO | StringIO, ?quiet: bool, **untyped) -> TaskProgress
78
+
79
+ # A root nobody runs a block for is finished when the program ends: done if it ends well,
80
+ # failed with the error if it is going down with one
81
+ # : (TaskProgress, output?) -> TaskProgress
82
+ def self.finish_at_exit: (TaskProgress, output?) -> TaskProgress
83
+
84
+ # Outputs that hold a thread or a screen - TerminalOutput, ExecutionLogOutput, BufferedOutput -
85
+ # can be closed, to draw their last and stop
86
+ # : (output?) -> void
87
+ def self.close: (output?) -> void
88
+
89
+ # A root whose tree is drawn in pages with Turbo, from wherever it runs - a job, say. Every
90
+ # frame is the whole tree as HTML (see HTMLRenderer), sent as a Turbo Stream at most every
91
+ # `interval` seconds, and kept in `store` for a page to draw before the first one arrives.
92
+ # Cancel buttons post to `cancel_url`; the request waits in `store`, where this checks for it
93
+ # every `poll` seconds - so it works across processes too. With a block, the block is the
94
+ # root's work, as with `cli`.
95
+ #
96
+ # TaskProgress.broadcast(csv_import, "Importing") { |root| ... } # in a Rails job
97
+ # <%= task_progress_tree @csv_import %> # in its page
98
+ #
99
+ # In Rails, the target is a record, and the rest comes from RailsDefaults: Turbo::StreamsChannel,
100
+ # Rails.cache and the route the Railtie adds. Outside Rails the target is the element's id, and
101
+ # `to:` - called with each frame - is up to you
102
+ # : (untyped, ?String, ?to: (^(String) -> void)?, ?store: TurboStreamOutput::_Store?, ?tokens: Token?, ?cancel_url: String?, ?renderer: TurboStreamOutput::_Renderer?, ?interval: Integer | Float, ?poll: Integer | Float, **untyped) -> TaskProgress
103
+ # : [T] (untyped, ?String, ?to: (^(String) -> void)?, ?store: TurboStreamOutput::_Store?, ?tokens: Token?, ?cancel_url: String?, ?renderer: TurboStreamOutput::_Renderer?, ?interval: Integer | Float, ?poll: Integer | Float, **untyped) { (TaskProgress) -> T } -> T?
104
+ def self.broadcast: (untyped, ?String, ?to: (^(String) -> void)?, ?store: TurboStreamOutput::_Store?, ?tokens: Token?, ?cancel_url: String?, ?renderer: TurboStreamOutput::_Renderer?, ?interval: Integer | Float, ?poll: Integer | Float, **untyped) -> TaskProgress
105
+ | [T] (untyped, ?String, ?to: (^(String) -> void)?, ?store: TurboStreamOutput::_Store?, ?tokens: Token?, ?cancel_url: String?, ?renderer: TurboStreamOutput::_Renderer?, ?interval: Integer | Float, ?poll: Integer | Float, **untyped) { (TaskProgress) -> T } -> T?
106
+
107
+ # Picks up the requests CancellationsApp leaves in the store, until the root has ended. A store
108
+ # that fails for a moment is tried again at the next poll
109
+ # : (TaskProgress, TurboStreamOutput::_Store, String, Integer | Float) -> Thread
110
+ def self.watch_cancellations: (TaskProgress, TurboStreamOutput::_Store, String, Integer | Float) -> Thread
111
+
112
+ # Where handed-off trees are kept, unless a call says otherwise - one connection, as
113
+ # `Rails.cache` is, and every tree in it a stream of its own under its own key.
114
+ #
115
+ # TaskProgress.stream_store = TaskProgress::RedisStreamStore.new(RedisClient.config(url: ENV["REDIS_URL"]).new_pool)
116
+ #
117
+ # Until it is set, a MemoryStreamStore: trees kept in this process alone, which is right for
118
+ # development and tests and quietly wrong anywhere a job runs in another process. So in
119
+ # production (RAILS_ENV, RACK_ENV or APP_ENV) it raises instead - set it to a MemoryStreamStore
120
+ # yourself if this process alone is what you mean
121
+ # : () -> untyped
122
+ def self.stream_store: () -> untyped
123
+
124
+ # Sets where handed-off trees are kept, see `stream_store`
125
+ # : (untyped) -> void
126
+ def self.stream_store=: (untyped) -> void
127
+
128
+ # Sets what signs the links `for_page` hands out. In Rails, the Railtie sets the app's own,
129
+ # made from its `secret_key_base`
130
+ # : (Token?) -> void
131
+ def self.tokens=: (Token?) -> void
132
+
133
+ # Where TreesApp is mounted, for `for_page` to link to - `/task_progress/trees` by default
134
+ # : () -> String
135
+ def self.trees_path: () -> String
136
+
137
+ # Sets where TreesApp is mounted
138
+ # : (String) -> void
139
+ def self.trees_path=: (String) -> void
140
+
141
+ # What signs the links `for_page` hands out - see `tokens=`
142
+ # : () -> Token
143
+ def self.tokens: () -> Token
144
+
145
+ # Everything a page needs to show a handed-off tree, as plain JSON-ready data - a prop for an
146
+ # Inertia or React page, a data attribute for Stimulus:
147
+ #
148
+ # render inertia: "Videos/Show", props: {progress: TaskProgress.for_page(video.progress_key)}
149
+ # # => {events: "/task_progress/trees/…/events", poll: "…/poll", cancel: "…/cancellations",
150
+ # # snapshot: {v: 1, …}}
151
+ #
152
+ # The links carry a Token good for `expires_in` seconds, not the tree's key: hand this out only
153
+ # where the page may see that tree. `snapshot` is the tree as it stands, so the page draws it
154
+ # before anything has come down the stream. TreesApp answers the links
155
+ # : (String | TaskProgress, ?expires_in: Integer, ?stream_store: untyped) -> Hash[Symbol, untyped]
156
+ def self.for_page: (String | TaskProgress, ?expires_in: Integer, ?stream_store: untyped) -> Hash[Symbol, untyped]
157
+
158
+ # A root to hand to whoever does the work - a job, a worker, another machine - which takes it
159
+ # up with `adopt`. Until then it waits, as "pending", where anyone can `follow` it: a page can
160
+ # show the job queued before any worker has picked it up.
161
+ #
162
+ # import = TaskProgress.handoff("Importing", cancelable: true)
163
+ # ImportJob.perform_later(upload, import) # ActiveJob takes the task itself
164
+ # Importer.perform_async(upload.id, import.key) # anything else, its key
165
+ #
166
+ # The tree lives in `stream_store`, under `key`. Once it is handed off, leave it to the worker: what
167
+ # this side writes after the worker has started is ignored
168
+ # : (String, ?stream_store: untyped, **untyped) -> TaskProgress
169
+ def self.handoff: (String, ?stream_store: untyped, **untyped) -> TaskProgress
170
+
171
+ # Takes up a tree handed off with `handoff` - by its key, or the task itself - as a root of
172
+ # its own, with the name and attributes it was handed off with. It starts the tree over: a
173
+ # second worker to adopt it, a job retried, replaces what the last one wrote.
174
+ #
175
+ # Batches go out squashed, every `interval` seconds at most. Every `heartbeat` seconds it tells
176
+ # whoever follows that it is alive, and takes the cancel requests that came. With a block,
177
+ # the block is the root's work, see `run`, and it all stops when the block returns. Without
178
+ # one, call `log.close` once the work is over - ActiveJob does this by itself, see JobSerializer
179
+ # : (String | TaskProgress, ?stream_store: untyped, ?heartbeat: Integer | Float, ?interval: Integer | Float) -> TaskProgress
180
+ # : [T] (String | TaskProgress, ?stream_store: untyped, ?heartbeat: Integer | Float, ?interval: Integer | Float) { (TaskProgress) -> T } -> T?
181
+ def self.adopt: (String | TaskProgress, ?stream_store: untyped, ?heartbeat: Integer | Float, ?interval: Integer | Float) -> TaskProgress
182
+ | [T] (String | TaskProgress, ?stream_store: untyped, ?heartbeat: Integer | Float, ?interval: Integer | Float) { (TaskProgress) -> T } -> T?
183
+
184
+ # Draws the tree handed off at `key` where a command-line program draws one (see `cli`) until it
185
+ # finishes, and returns it. Ctrl-C stops watching, and leaves the work alone. What
186
+ # `task_progress watch` does
187
+ # : (String, ?stream_store: untyped, ?io: IO | StringIO) -> TaskProgress
188
+ def self.watch: (String, ?stream_store: untyped, ?io: IO | StringIO) -> TaskProgress
189
+
190
+ # A tree that mirrors the one in `stream_store` at `key`, kept up to date on a thread of its own -
191
+ # for drawing it anywhere: a terminal, a parent's tree, an endpoint of your own. Requests to
192
+ # cancel its tasks go to whoever writes it; it goes stale when the writer goes quiet, and fails
193
+ # after `give_up_after` seconds of that, see Mount#follow_stream. With a block, it follows for
194
+ # as long as the block runs; without one, `log.close` stops it. SSE and Poll take the key
195
+ # itself, and follow for as long as they need to. A key nothing was handed off at is a KeyError
196
+ # : (String, ?stream_store: untyped, ?poll: Integer | Float, ?give_up_after: Integer | Float) -> TaskProgress
197
+ # : [T] (String, ?stream_store: untyped, ?poll: Integer | Float, ?give_up_after: Integer | Float) { (TaskProgress) -> T } -> T
198
+ def self.follow: (String, ?stream_store: untyped, ?poll: Integer | Float, ?give_up_after: Integer | Float) -> TaskProgress
199
+ | [T] (String, ?stream_store: untyped, ?poll: Integer | Float, ?give_up_after: Integer | Float) { (TaskProgress) -> T } -> T
200
+
201
+ # The tree in `stream_store` at `key` as it is now, read once - for a page, or one poll, that needs
202
+ # no more than that. See `follow` for one kept up to date. A key nothing was handed off at -
203
+ # or whose tree has expired - is a KeyError, as with Hash#fetch
204
+ # : (String, ?stream_store: untyped) -> TaskProgress
205
+ def self.fetch: (String, ?stream_store: untyped) -> TaskProgress
206
+
207
+ # Runs the block as the work of every root among `arguments` that a worker took up with `adopt`
208
+ # - done when it returns, failed when it raises - and closes them after. What JobSerializer
209
+ # wraps every ActiveJob in; for any other job runner, wrap `perform` in it the same way
210
+ # : [T] (Array[untyped]) { () -> T } -> T?
211
+ def self.working_on: [T] (Array[untyped]) { () -> T } -> T?
212
+
213
+ # The block as the work of each root in turn, the first outermost
214
+ # : [T] (Array[TaskProgress]) { () -> T } -> T?
215
+ def self.within: [T] (Array[TaskProgress]) { () -> T } -> T?
216
+
217
+ # A root that is a mount and nothing else, named after `key` until the stream says otherwise
218
+ # : (String, untyped) -> [TaskProgress, Mount]
219
+ def self.mirror: (String, untyped) -> [ TaskProgress, Mount ]
220
+
221
+ # A size that is cheap to know, or nil - never `count`, which would run a lazy enumerator
222
+ # : (untyped) -> Integer?
223
+ def self.size_of: (untyped) -> Integer?
224
+
225
+ # Where the browser client lives: `task_tree.js` (the fold, and `follow` for an SSE stream),
226
+ # `render_queue.js`, `row_view.js`, a Stimulus controller, a React component and
227
+ # `task_tree.css`. No build step - serve the directory as it is, under any prefix
228
+ # : () -> String
229
+ def self.js_path: () -> String
230
+
231
+ # Copies the browser client into `dir`, for a bundler - Vite, esbuild - to take from there, and
232
+ # returns the names of the files it wrote. The copy is a package of its own, named
233
+ # `task_progress`: `npm install ./dir` puts it in `node_modules`, TypeScript declarations and all.
234
+ #
235
+ # `dir` is the gem's: whatever an earlier vendoring left there is replaced, so a file the gem
236
+ # no longer ships goes too. A directory with files of its own in it is left alone, and raises
237
+ # : (String) -> Array[String]
238
+ def self.vendor_js: (String) -> Array[String]
239
+
240
+ # The version of the client vendored into `dir`, or nil when there is none
241
+ # : (String) -> String?
242
+ def self.vendored_js_version: (String) -> String?
243
+
244
+ # A silent root, for a library's default
245
+ # : (?String) -> TaskProgress
246
+ def self.null: (?String) -> TaskProgress
247
+
248
+ # A new tree, and its root. See `subtask` for `autofinish`
249
+ # : (?String, ?outputs: Array[output], ?log: Log, ?total: Integer?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) -> void
250
+ def initialize: (?String, ?outputs: Array[output], ?log: Log, ?total: Integer?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) -> void
251
+
252
+ # The attributes an `open` carries, without what is at its default
253
+ # : (name: String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?attempt: Integer?) -> Hash[Symbol, untyped]
254
+ def self.attributes: (name: String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?attempt: Integer?) -> Hash[Symbol, untyped]
255
+
256
+ # A handle on the task at `path` in `log`, without opening anything
257
+ # : (Log, path) -> TaskProgress
258
+ def self.handle: (Log, path) -> TaskProgress
259
+
260
+ # The stream key a root from `handoff`, `adopt`, `follow` or `fetch` lives under, which is
261
+ # all it takes to `adopt` or `follow` it from anywhere. Nil for any other task
262
+ # : () -> String?
263
+ def key: () -> String?
264
+
265
+ # The task and everything under it as plain Hashes - its name, status and percent, and its
266
+ # count, total, message and error where it has them - for a test to compare, or a log line:
267
+ #
268
+ # {name: "Import", status: :done, percent: 100, children: [
269
+ # {name: "Rows", status: :failed, percent: 40, count: 4, total: 10, error: "RuntimeError: row 5"}]}
270
+ # : () -> Hash[Symbol, untyped]
271
+ def outline: () -> Hash[Symbol, untyped]
272
+
273
+ # The node at `path` in `state` as an outline, see TaskProgress#outline
274
+ # : (State, path) -> Hash[Symbol, untyped]
275
+ def self.outline: (State, path) -> Hash[Symbol, untyped]
276
+
277
+ # Which run of the work this is, for a root taken up with `adopt`: 1, then 2 for a retry, and
278
+ # so on. Drawn with the task from 2 on. Nil for any other task
279
+ # : () -> Integer?
280
+ def attempt: () -> Integer?
281
+
282
+ # The task as the tree holds it now - frozen, and safe to read from any thread
283
+ # : () -> Node
284
+ def node: () -> Node
285
+
286
+ # What it is called
287
+ # : () -> String
288
+ def name: () -> String
289
+
290
+ # `:pending`, `:active`, `:done`, `:failed` or `:canceled`
291
+ # : () -> Symbol
292
+ def status: () -> Symbol
293
+
294
+ # A whole percent, 100 only once it and everything under it are done
295
+ # : () -> Integer
296
+ def percent: () -> Integer
297
+
298
+ # How far along, 0.0..1.0, unrounded
299
+ # : () -> Float
300
+ def fraction: () -> Float
301
+
302
+ # The latest thing it said, see `note`
303
+ # : () -> String?
304
+ def message: () -> String?
305
+
306
+ # What it failed with: `class`, `message`, `backtrace` and `cause`, see `fail!`
307
+ # : () -> Hash[Symbol, untyped]?
308
+ def error: () -> Hash[Symbol, untyped]?
309
+
310
+ # How many it has counted, see `advance`
311
+ # : () -> Integer
312
+ def count: () -> Integer
313
+
314
+ # How many it counts to, or nil when it has no total
315
+ # : () -> Integer?
316
+ def total: () -> Integer?
317
+
318
+ # Done, and everything under it ended
319
+ # : () -> bool
320
+ def complete?: () -> bool
321
+
322
+ # Ended by `fail!`
323
+ # : () -> bool
324
+ def failed?: () -> bool
325
+
326
+ # Ended by `cancel!`, or by a task above it being canceled
327
+ # : () -> bool
328
+ def canceled?: () -> bool
329
+
330
+ # Whether a reader may offer to call it off, and it has not ended yet
331
+ # : () -> bool
332
+ def cancelable?: () -> bool
333
+
334
+ # The task at `path` under this one - an Array of ids, or the ids joined with a dash, the way
335
+ # the browser client keys its rows (`x3k0-00af`) - or nil
336
+ # : (path | String) -> TaskProgress?
337
+ def find: (path | String) -> TaskProgress?
338
+
339
+ # Ended, by `done!`, `fail!` or `cancel!` - whether or not everything under it did
340
+ # : () -> bool
341
+ def finished?: () -> bool
342
+
343
+ # The task it is under, or nil for the root
344
+ # : () -> TaskProgress?
345
+ def parent: () -> TaskProgress?
346
+
347
+ # The root of its tree
348
+ # : () -> TaskProgress
349
+ def root: () -> TaskProgress
350
+
351
+ # The tasks directly under it, in the order they opened
352
+ # : () -> Array[TaskProgress]
353
+ def subtasks: () -> Array[TaskProgress]
354
+
355
+ # Sends this task's subtree to an output, as a tree of its own rooted at `[]` - for the root
356
+ # that is the whole tree. See Log#add_output
357
+ # : (?output?, ?replay: bool) ?{ (Batch, Log) -> void } -> output
358
+ def add_output: (?output?, ?replay: bool) ?{ (Batch, Log) -> void } -> output
359
+
360
+ # Stops sending to an output, as it was given to `add_output`
361
+ # : (output) -> void
362
+ def remove_output: (output) -> void
363
+
364
+ # A child task. `total:` makes it counted (drive it with `advance`), `indeterminate: true`
365
+ # one with no measure of its own, and `weight:` how much of the parent it is. With a block
366
+ # it runs the block as the child's work, see `run`, and returns the block's value.
367
+ #
368
+ # A counted task is done once its count reaches its total. Give it `autofinish: false` when
369
+ # the total is a guess, and the work may go past it.
370
+ #
371
+ # `cancelable: true` tells whoever draws the tree that it may offer to call the task off - see
372
+ # `find` and `cancel!` for taking that request back
373
+ # : (String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) -> TaskProgress
374
+ # : [T] (String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) { (TaskProgress) -> T } -> T?
375
+ def subtask: (String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) -> TaskProgress
376
+ | [T] (String, ?total: Integer?, ?weight: (Integer | Float)?, ?expect: Integer?, ?indeterminate: bool, ?unit: String?, ?cancelable: bool, ?autofinish: bool) { (TaskProgress) -> T } -> T?
377
+
378
+ # Runs the block as this task's work: done when it returns, failed with the error when it
379
+ # raises - an Interrupt or a SignalException included - and the error raised on. A task
380
+ # canceled ends its work quietly: `run` returns nil. One canceled before it began does not
381
+ # begin
382
+ # : [T] () { (TaskProgress) -> T } -> T?
383
+ def run: [T] () { (TaskProgress) -> T } -> T?
384
+
385
+ # Moves the percent; 100 finishes the task
386
+ # : (Integer | Float, ?message: String?) -> self
387
+ def update: (Integer | Float, ?message: String?) -> self
388
+
389
+ # Adds to the count - finishing the task, if that reaches its total
390
+ # : (?Integer, ?message: String?) -> self
391
+ def advance: (?Integer, ?message: String?) -> self
392
+
393
+ # Adds one to the count, see `advance`
394
+ # : (?message: String?) -> self
395
+ def tick: (?message: String?) -> self
396
+
397
+ # Goes through a collection a step per item. Given a name it opens a subtask for it, and
398
+ # otherwise counts on this task. The total comes from `size`, never `count` - so a lazy
399
+ # enumerator stays lazy, and one with no size just counts. Done when the loop ends, failed
400
+ # with the error if the block raises. Without a block it returns an Enumerator, which does
401
+ # the same as it is consumed
402
+ #
403
+ # root.iterate(files, "Files") { |file| copy(file) }
404
+ # root.iterate(rows, "Rows").map { transform(it) }
405
+ # : (Enumerable[untyped], ?String?, ?total: Integer?, ?step: Integer) ?{ (untyped) -> untyped } -> untyped
406
+ def iterate: (Enumerable[untyped], ?String?, ?total: Integer?, ?step: Integer) ?{ (untyped) -> untyped } -> untyped
407
+
408
+ # Prints a line above the tree, where it stays. It changes nothing in the tree itself, so a
409
+ # reader joining later never sees it - like anything said on a terminal
410
+ # : (_ToS) -> self
411
+ def puts: (_ToS) -> self
412
+
413
+ # The latest thing it says, shown on its row
414
+ # : (String?) -> self
415
+ def note: (String?) -> self
416
+
417
+ # Changes what it counts to
418
+ # : (Integer?) -> void
419
+ def total=: (Integer?) -> void
420
+
421
+ # Renames it
422
+ # : (String) -> void
423
+ def name=: (String) -> void
424
+
425
+ # Finishes the task, and with it every task under it still open - each with a `done` of its
426
+ # own, deepest first, in the same batch. The work they were part of is over, and a reader
427
+ # should not be left waiting on a task nobody will ever finish. What was mounted is left to
428
+ # its Mount
429
+ # : (?String?) -> self
430
+ def done!: (?String?) -> self
431
+
432
+ # Fails the task with an exception, a message, or nothing. `extra` goes along with the error:
433
+ # up to 8 of them, each true, false or an Integer, which is all a reader takes
434
+ #
435
+ # What is still open under it is canceled: it did not fail, it was cut short
436
+ # : (?(Exception | String)?, **bool | Integer) -> self
437
+ def fail!: (?(Exception | String)?, **bool | Integer) -> self
438
+
439
+ # Calls the task off, and every task still open under it, each with a `cancel` of its own -
440
+ # from any thread. Whoever works on one of them next gets a Canceled, which `run` takes as
441
+ # the end of that task's work. Canceling a finished task does nothing
442
+ # : (?String?) -> self
443
+ def cancel!: (?String?) -> self
444
+
445
+ # Calls the block with the reason once this task is canceled, see Log#on_cancel. For work that
446
+ # blocks and so would not notice a cancel until it next touches its task:
447
+ #
448
+ # rows.on_cancel { http.finish }
449
+ # : () { (String?) -> void } -> self
450
+ def on_cancel: () { (String?) -> void } -> self
451
+
452
+ # Asks for the task at `path` under this one to be canceled - the way a request from outside
453
+ # (a browser, a parent process) comes in. Everything is decided in one transaction against the
454
+ # tree as it stands, so it cannot race the task finishing. Returns:
455
+ #
456
+ # :canceled it was, along with everything still open under it
457
+ # :forwarded it lives in a mounted child, which was asked; its cancel comes back up the stream
458
+ # :moot there is nothing left to cancel - no such task, or it has already ended
459
+ # :refused it is not cancelable
460
+ #
461
+ # A request that comes too late is moot, not an error: by the time it arrives the task may well
462
+ # have finished, and its process exited
463
+ # : (path | String, ?String?) -> Symbol
464
+ def request_cancel: (path | String, ?String?) -> Symbol
465
+
466
+ # Reads cancel requests off an IO - one JSON line each, `{"cancel": [ids], "reason": "..."}` -
467
+ # on a thread of its own, and hands them to `request_cancel`. This is the far end of a Mount's
468
+ # control pipe; anything malformed is skipped. Returns the thread, which ends at EOF.
469
+ #
470
+ # The root itself, `[]`, is what the parent mounted, and only the parent asks for it: the
471
+ # mount was ended there, so the root is canceled whether it is cancelable or not
472
+ # : (IO) -> Thread
473
+ def accept_cancel_requests: (IO) -> Thread
474
+
475
+ # Takes one cancel request - a line as `accept_cancel_requests` reads them - and returns what
476
+ # came of it, see `request_cancel`; nil for a line that is not a request
477
+ # : (String) -> Symbol?
478
+ def take_cancel_request: (String) -> Symbol?
479
+
480
+ # The reason an Interrupt gives, if it gives one: Ctrl-C raises it saying "Interrupt", and a
481
+ # SIGINT from outside saying nothing at all
482
+ # : (Interrupt) -> String?
483
+ def self.interrupted_by: (Interrupt) -> String?
484
+
485
+ # Every task called off carries the reason, so each of them can say why it stopped
486
+ # : (Transaction, path, String?) -> void
487
+ def self.cancel_in: (Transaction, path, String?) -> void
488
+
489
+ # Everything the block does to the tree lands as one batch, or none of it does
490
+ # : [T] () { (TaskProgress) -> T } -> T
491
+ def transaction: [T] () { (TaskProgress) -> T } -> T
492
+
493
+ # A child whose subtree is fed from elsewhere - a subprocess, a Ractor, a stream. Only the
494
+ # Mount writes under it, see Mount
495
+ # : (String, ?weight: (Integer | Float)?, ?cancelable: bool) -> Mount
496
+ def mount: (String, ?weight: (Integer | Float)?, ?cancelable: bool) -> Mount
497
+
498
+ # Wraps an IO so that reading it advances this task by the bytes read, see TrackedIO. The total
499
+ # is the IO's size unless given; with neither, the task counts bytes with no measure. With a
500
+ # block, the block runs with the wrapped IO as this task's work, see `run`. Needs the
501
+ # progressive_io gem, and an IO that can tell its position - for a pipe or a socket, call
502
+ # `advance` with each chunk instead
503
+ # : (untyped, ?total: Integer?, ?step: Integer?) -> TrackedIO
504
+ # : [T] (untyped, ?total: Integer?, ?step: Integer?) { (TrackedIO) -> T } -> T?
505
+ def track_io: (untyped, ?total: Integer?, ?step: Integer?) -> TrackedIO
506
+ | [T] (untyped, ?total: Integer?, ?step: Integer?) { (TrackedIO) -> T } -> T?
507
+
508
+ # The same task in the same tree
509
+ # : (untyped) -> bool
510
+ def ==: (untyped) -> bool
511
+
512
+ # Equal handles hash alike, so a task works as a Hash key
513
+ # : () -> Integer
514
+ def hash: () -> Integer
515
+
516
+ # Its path, name and status
517
+ # : () -> String
518
+ def inspect: () -> String
519
+
520
+ SCALE: Array[[ String, Integer ]]
521
+
522
+ # "4.2s", "3m07s", "2h15m"
523
+ # : (Integer | Float) -> String
524
+ def self.duration: (Integer | Float) -> String
525
+
526
+ # "142 KB", "2.9 MB", "640 bytes" - a whole number drops its decimal
527
+ # : (Integer | Float) -> String
528
+ def self.human_bytes: (Integer | Float) -> String
529
+
530
+ # Every message reaches the tree as valid UTF-8. Messages often name files, and a raw
531
+ # ZIP read hands those back tagged ASCII-8BIT, so a "Reading Alice’s.xhtml" would
532
+ # otherwise blow up whatever an output writes to (a database column, a UTF-8 log line)
533
+ # : (nil) -> nil
534
+ # : (untyped) -> String
535
+ def self.utf8: (nil) -> nil
536
+ | (untyped) -> String
537
+
538
+ private
539
+
540
+ # : (**untyped) -> self
541
+ def meta: (**untyped) -> self
542
+
543
+ # Finishing a finished task is a no-op; anything else on one raises, as does writing under
544
+ # a mount from here
545
+ # : (Transaction, ?finishing: bool) -> bool
546
+ def writable!: (Transaction, ?finishing: bool) -> bool
547
+
548
+ # : (State, ?above: bool) -> bool
549
+ def sealed?: (State, ?above: bool) -> bool
550
+
551
+ # : (untyped) -> bool
552
+ def positioned?: (untyped) -> bool
553
+ end