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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE +21 -0
- data/MANUAL.md +851 -0
- data/README.md +158 -0
- data/RECIPES.md +413 -0
- data/config/importmap.rb +17 -0
- data/exe/task_progress +51 -0
- data/js/local_tasks.d.ts +44 -0
- data/js/local_tasks.js +229 -0
- data/js/package.json +31 -0
- data/js/paths.d.ts +5 -0
- data/js/paths.js +7 -0
- data/js/react_task_tree.d.ts +49 -0
- data/js/react_task_tree.js +123 -0
- data/js/render_queue.d.ts +24 -0
- data/js/render_queue.js +79 -0
- data/js/row_view.d.ts +25 -0
- data/js/row_view.js +69 -0
- data/js/stimulus_controller.d.ts +10 -0
- data/js/stimulus_controller.js +137 -0
- data/js/task_progress_clock.d.ts +2 -0
- data/js/task_progress_clock.js +33 -0
- data/js/task_tree.css +51 -0
- data/js/task_tree.d.ts +111 -0
- data/js/task_tree.js +444 -0
- data/lib/task_progress/assertions.rb +49 -0
- data/lib/task_progress/batch.rb +23 -0
- data/lib/task_progress/branch.rb +74 -0
- data/lib/task_progress/buffered_output.rb +63 -0
- data/lib/task_progress/cancellations_app.rb +76 -0
- data/lib/task_progress/event.rb +46 -0
- data/lib/task_progress/execution_log_output.rb +183 -0
- data/lib/task_progress/fold.rb +18 -0
- data/lib/task_progress/html_renderer.rb +153 -0
- data/lib/task_progress/job_serializer.rb +92 -0
- data/lib/task_progress/journal.rb +35 -0
- data/lib/task_progress/log.rb +263 -0
- data/lib/task_progress/memory_cache.rb +31 -0
- data/lib/task_progress/memory_stream_store.rb +142 -0
- data/lib/task_progress/mount.rb +448 -0
- data/lib/task_progress/ndjson_output.rb +24 -0
- data/lib/task_progress/node.rb +52 -0
- data/lib/task_progress/poll.rb +87 -0
- data/lib/task_progress/publishing_output.rb +30 -0
- data/lib/task_progress/ractor_output.rb +23 -0
- data/lib/task_progress/rails_defaults.rb +46 -0
- data/lib/task_progress/railtie.rb +91 -0
- data/lib/task_progress/recording_output.rb +51 -0
- data/lib/task_progress/redis_stream_store.rb +131 -0
- data/lib/task_progress/squasher.rb +59 -0
- data/lib/task_progress/sse.rb +114 -0
- data/lib/task_progress/state.rb +319 -0
- data/lib/task_progress/stream_output.rb +222 -0
- data/lib/task_progress/stream_reader.rb +76 -0
- data/lib/task_progress/summary_output.rb +37 -0
- data/lib/task_progress/terminal_output.rb +241 -0
- data/lib/task_progress/token.rb +58 -0
- data/lib/task_progress/tracked_io.rb +68 -0
- data/lib/task_progress/transaction.rb +41 -0
- data/lib/task_progress/trees_app.rb +66 -0
- data/lib/task_progress/turbo_helper.rb +18 -0
- data/lib/task_progress/turbo_sse.rb +103 -0
- data/lib/task_progress/turbo_stream_output.rb +61 -0
- data/lib/task_progress/version.rb +7 -0
- data/lib/task_progress/wire.rb +211 -0
- data/lib/task_progress.rb +1036 -0
- data/sig/generated/task_progress/assertions.rbs +36 -0
- data/sig/generated/task_progress/batch.rbs +33 -0
- data/sig/generated/task_progress/branch.rbs +47 -0
- data/sig/generated/task_progress/buffered_output.rbs +38 -0
- data/sig/generated/task_progress/cancellations_app.rbs +41 -0
- data/sig/generated/task_progress/event.rbs +20 -0
- data/sig/generated/task_progress/execution_log_output.rbs +93 -0
- data/sig/generated/task_progress/fold.rbs +25 -0
- data/sig/generated/task_progress/html_renderer.rbs +71 -0
- data/sig/generated/task_progress/journal.rbs +25 -0
- data/sig/generated/task_progress/log.rbs +140 -0
- data/sig/generated/task_progress/memory_cache.rbs +28 -0
- data/sig/generated/task_progress/memory_stream_store.rbs +83 -0
- data/sig/generated/task_progress/mount.rbs +200 -0
- data/sig/generated/task_progress/ndjson_output.rbs +18 -0
- data/sig/generated/task_progress/node.rbs +66 -0
- data/sig/generated/task_progress/poll.rbs +50 -0
- data/sig/generated/task_progress/publishing_output.rbs +24 -0
- data/sig/generated/task_progress/ractor_output.rbs +17 -0
- data/sig/generated/task_progress/rails_defaults.rbs +33 -0
- data/sig/generated/task_progress/recording_output.rbs +41 -0
- data/sig/generated/task_progress/redis_stream_store.rbs +75 -0
- data/sig/generated/task_progress/squasher.rbs +20 -0
- data/sig/generated/task_progress/sse.rbs +73 -0
- data/sig/generated/task_progress/state.rbs +165 -0
- data/sig/generated/task_progress/stream_output.rbs +142 -0
- data/sig/generated/task_progress/stream_reader.rbs +46 -0
- data/sig/generated/task_progress/summary_output.rbs +19 -0
- data/sig/generated/task_progress/terminal_output.rbs +117 -0
- data/sig/generated/task_progress/token.rbs +41 -0
- data/sig/generated/task_progress/tracked_io.rbs +48 -0
- data/sig/generated/task_progress/transaction.rbs +33 -0
- data/sig/generated/task_progress/trees_app.rbs +40 -0
- data/sig/generated/task_progress/turbo_helper.rbs +14 -0
- data/sig/generated/task_progress/turbo_sse.rbs +69 -0
- data/sig/generated/task_progress/turbo_stream_output.rbs +51 -0
- data/sig/generated/task_progress/version.rbs +6 -0
- data/sig/generated/task_progress/wire.rbs +102 -0
- data/sig/generated/task_progress.rbs +553 -0
- 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
|