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,36 @@
|
|
|
1
|
+
# Generated from lib/task_progress/assertions.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Assertions for Minitest - and ActiveSupport::TestCase, which is Minitest underneath - about
|
|
5
|
+
# what work reported. Each failure shows the whole tree, see TaskProgress#outline.
|
|
6
|
+
#
|
|
7
|
+
# class ImportTest < Minitest::Test
|
|
8
|
+
# include TaskProgress::Assertions
|
|
9
|
+
#
|
|
10
|
+
# def test_import
|
|
11
|
+
# import = TaskProgress.new("Import")
|
|
12
|
+
# Importer.new(progress: import).run
|
|
13
|
+
# assert_task_complete import
|
|
14
|
+
# assert_task_failed import.subtasks.first, with: /row 12/
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
module Assertions
|
|
18
|
+
def assert: (untyped, ?String) -> void
|
|
19
|
+
|
|
20
|
+
# The task is done, and everything under it ended
|
|
21
|
+
# : (TaskProgress, ?String?) -> void
|
|
22
|
+
def assert_task_complete: (TaskProgress, ?String?) -> void
|
|
23
|
+
|
|
24
|
+
# The task failed - with an error whose message matches `with`, if given
|
|
25
|
+
# : (TaskProgress, ?with: (String | Regexp)?, ?message: String?) -> void
|
|
26
|
+
def assert_task_failed: (TaskProgress, ?with: (String | Regexp)?, ?message: String?) -> void
|
|
27
|
+
|
|
28
|
+
# The task was canceled
|
|
29
|
+
# : (TaskProgress, ?String?) -> void
|
|
30
|
+
def assert_task_canceled: (TaskProgress, ?String?) -> void
|
|
31
|
+
|
|
32
|
+
# The tree, as a failure message ends with it
|
|
33
|
+
# : (TaskProgress) -> String
|
|
34
|
+
def self.tree: (TaskProgress) -> String
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated from lib/task_progress/batch.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# The unit of commit, see below
|
|
5
|
+
class Batch < Data
|
|
6
|
+
attr_reader log(): String?
|
|
7
|
+
|
|
8
|
+
attr_reader prev(): Integer
|
|
9
|
+
|
|
10
|
+
attr_reader seq(): Integer
|
|
11
|
+
|
|
12
|
+
attr_reader at(): Float
|
|
13
|
+
|
|
14
|
+
attr_reader events(): Array[Event]
|
|
15
|
+
|
|
16
|
+
def self.new: (String? log, Integer prev, Integer seq, Float at, Array[Event] events) -> instance
|
|
17
|
+
| (log: String?, prev: Integer, seq: Integer, at: Float, events: Array[Event]) -> instance
|
|
18
|
+
|
|
19
|
+
def self.members: () -> [ :log, :prev, :seq, :at, :events ]
|
|
20
|
+
|
|
21
|
+
def members: () -> [ :log, :prev, :seq, :at, :events ]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The unit of commit: everything one transaction changed, in order. `seq` counts batches,
|
|
25
|
+
# not events, and `prev` names the batch it follows - so a reader that sees a `prev` other
|
|
26
|
+
# than the `seq` it last saw knows it missed something. `prev` 0 means "from the start",
|
|
27
|
+
# which is also how a squashed replay announces itself
|
|
28
|
+
class Batch
|
|
29
|
+
# Whether it starts over from nothing, as a squash does
|
|
30
|
+
# : () -> bool
|
|
31
|
+
def reset?: () -> bool
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Generated from lib/task_progress/branch.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# An output for one subtree, which it sees as a tree of its own: the branch's node becomes the
|
|
5
|
+
# root, `[*branch, "x3k0"]` becomes `["x3k0"]`, and batches that do not touch the branch are left
|
|
6
|
+
# out. That is a Mount run backwards - a Mount fed from a Branch grafts the subtree back whole.
|
|
7
|
+
#
|
|
8
|
+
# Since batches get left out, a branch numbers its own: `log` is the log's id with the branch
|
|
9
|
+
# path appended, and its `prev`/`seq` follow on from each other just like the log's do.
|
|
10
|
+
#
|
|
11
|
+
# Pruning an ancestor takes the branch's node away. Usually for good, since a pruned path is
|
|
12
|
+
# never given out again - and the branch then sends one last reset with nothing in it, and
|
|
13
|
+
# falls silent. But a Mount starting over (a remote that restarted, a stream resynced) prunes
|
|
14
|
+
# its subtree and puts the same tasks back in the same batch: the branch's node comes back,
|
|
15
|
+
# and the branch starts over with it.
|
|
16
|
+
#
|
|
17
|
+
# Its first batch always starts from nothing, `prev` 0: one for a node that comes into being
|
|
18
|
+
# after the branch was made carries the node's `open`
|
|
19
|
+
class Branch
|
|
20
|
+
# The output the branch feeds
|
|
21
|
+
attr_reader output: output
|
|
22
|
+
|
|
23
|
+
# The branch's task, in the whole tree
|
|
24
|
+
attr_reader path: path
|
|
25
|
+
|
|
26
|
+
# The log id its batches carry: the log's, with the path appended
|
|
27
|
+
attr_reader id: String
|
|
28
|
+
|
|
29
|
+
@seq: Integer
|
|
30
|
+
|
|
31
|
+
@gone: bool
|
|
32
|
+
|
|
33
|
+
@started: bool
|
|
34
|
+
|
|
35
|
+
# The log id of the branch at `path`
|
|
36
|
+
# : (String, path) -> String
|
|
37
|
+
def self.id: (String, path) -> String
|
|
38
|
+
|
|
39
|
+
# For an output, the branch at `path` of the log `log_id`, numbering on from `seq`
|
|
40
|
+
# : (output, path, String, Integer, ?started: bool) -> void
|
|
41
|
+
def initialize: (output, path, String, Integer, ?started: bool) -> void
|
|
42
|
+
|
|
43
|
+
# Passes on what of the batch is in the branch, moved to its root
|
|
44
|
+
# : (Batch, Log) -> void
|
|
45
|
+
def call: (Batch, Log) -> void
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Generated from lib/task_progress/buffered_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Puts a thread between the log and a slow output. The first batch goes through at once,
|
|
5
|
+
# and whatever piles up while the output works (or during `interval` after it) goes out
|
|
6
|
+
# squashed into one - so a database write a few times a second keeps up with any number of
|
|
7
|
+
# workers, and never holds one of them up
|
|
8
|
+
class BufferedOutput
|
|
9
|
+
@output: output
|
|
10
|
+
|
|
11
|
+
@interval: Integer | Float
|
|
12
|
+
|
|
13
|
+
@queue: Thread::Queue[Batch]
|
|
14
|
+
|
|
15
|
+
@log: Log?
|
|
16
|
+
|
|
17
|
+
@squasher: Squasher
|
|
18
|
+
|
|
19
|
+
@thread: Thread
|
|
20
|
+
|
|
21
|
+
# For the slow output, sending to it at most every `interval` seconds
|
|
22
|
+
# : (output, ?interval: Integer | Float) -> void
|
|
23
|
+
def initialize: (output, ?interval: Integer | Float) -> void
|
|
24
|
+
|
|
25
|
+
# Takes one batch, to deliver from its own thread
|
|
26
|
+
# : (Batch, Log) -> void
|
|
27
|
+
def call: (Batch, Log) -> void
|
|
28
|
+
|
|
29
|
+
# Delivers what is still queued, then stops
|
|
30
|
+
# : () -> void
|
|
31
|
+
def close: () -> void
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# : () -> void
|
|
36
|
+
def run: () -> void
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated from lib/task_progress/cancellations_app.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A Rack app that takes the cancel buttons HTMLRenderer draws, for a tree that lives in another
|
|
5
|
+
# process - a job broadcasting with TaskProgress.broadcast. It cannot cancel anything itself:
|
|
6
|
+
# it checks the request's token, and leaves the request in the store the job watches. The job
|
|
7
|
+
# decides, as it does for any request (see TaskProgress#request_cancel), and the cancel comes
|
|
8
|
+
# back in its next frame.
|
|
9
|
+
#
|
|
10
|
+
# A Turbo form submission is answered with an empty Turbo Stream, which changes nothing on the
|
|
11
|
+
# page - the frame that follows does. Anything else gets 202 and a little JSON
|
|
12
|
+
class CancellationsApp
|
|
13
|
+
# How long a request waits for its run to take it up
|
|
14
|
+
REQUEST_TTL: Integer
|
|
15
|
+
|
|
16
|
+
@store: TurboStreamOutput::_Store
|
|
17
|
+
|
|
18
|
+
@tokens: Token
|
|
19
|
+
|
|
20
|
+
# Leaves requests in `store`, for tokens `tokens` issued
|
|
21
|
+
# : (store: TurboStreamOutput::_Store, tokens: Token) -> void
|
|
22
|
+
def initialize: (store: TurboStreamOutput::_Store, tokens: Token) -> void
|
|
23
|
+
|
|
24
|
+
# Takes a POST with `path` - the task's ids joined with a dash - and `token`, as a form or JSON
|
|
25
|
+
# : (Hash[String, untyped]) -> [Integer, Hash[String, String], Array[String]]
|
|
26
|
+
def call: (Hash[String, untyped]) -> [ Integer, Hash[String, String], Array[String] ]
|
|
27
|
+
|
|
28
|
+
# Where a request to cancel the task at `path` waits, for the tree stored under `key`
|
|
29
|
+
# : (String, path) -> String
|
|
30
|
+
def self.request_key: (String, path) -> String
|
|
31
|
+
|
|
32
|
+
# The request's parameters, from a form or JSON
|
|
33
|
+
# : (Hash[String, untyped]) -> Hash[String, untyped]
|
|
34
|
+
def self.params: (Hash[String, untyped]) -> Hash[String, untyped]
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# : (Integer, Hash[Symbol, untyped]) -> [Integer, Hash[String, String], Array[String]]
|
|
39
|
+
def respond: (Integer, Hash[Symbol, untyped]) -> [ Integer, Hash[String, String], Array[String] ]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Generated from lib/task_progress/event.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
class Event < Data
|
|
5
|
+
attr_reader path: path
|
|
6
|
+
attr_reader op: Symbol
|
|
7
|
+
attr_reader value: untyped
|
|
8
|
+
def self.new: (path: path, op: Symbol, ?value: untyped) -> instance
|
|
9
|
+
def with: (?path: path, ?op: Symbol, ?value: untyped) -> instance
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Event
|
|
13
|
+
# An event for the node at `path` - its path is frozen, so the event can be shared
|
|
14
|
+
# : (path: path, op: Symbol, ?value: untyped) -> void
|
|
15
|
+
def initialize: (path: path, op: Symbol, ?value: untyped) -> void
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Every op there is - see Event
|
|
19
|
+
OPS: Array[Symbol]
|
|
20
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Generated from lib/task_progress/execution_log_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# For a CI log, or anything else that is read top to bottom after the fact: one finished
|
|
5
|
+
# line per thing worth saying, never a redraw, each line led by the overall percent and
|
|
6
|
+
# the time since the root opened.
|
|
7
|
+
#
|
|
8
|
+
# [ 42% 1m02s] Import › Rows: row 12000
|
|
9
|
+
# [ 50% 1m10s] ✓ Import › Rows 8.1s
|
|
10
|
+
# [ 50% 1m11s] ✗ Import › Engine: watermarking failed
|
|
11
|
+
# [ 50% 1m12s] ⊘ Import › Publish: Ctrl-C
|
|
12
|
+
#
|
|
13
|
+
# A task finishing, failing or being canceled, and a line it `puts`, go out at once and every
|
|
14
|
+
# time - though of a task and the ones under it ending in one batch, only the task says so. The
|
|
15
|
+
# rest - notes and counts - are only ever the latest news, so it says at most one of those
|
|
16
|
+
# per `interval`, the newest. With nothing to say for `heartbeat` it says it is still
|
|
17
|
+
# going, so a CI runner watching for output does not take a long quiet step for a hang
|
|
18
|
+
class ExecutionLogOutput
|
|
19
|
+
@ended: Set[path]
|
|
20
|
+
|
|
21
|
+
@thread: Thread?
|
|
22
|
+
|
|
23
|
+
@closed: bool
|
|
24
|
+
|
|
25
|
+
@said_at: Float
|
|
26
|
+
|
|
27
|
+
@pending: [ path, String? ]?
|
|
28
|
+
|
|
29
|
+
@log: Log?
|
|
30
|
+
|
|
31
|
+
@wake: Thread::ConditionVariable
|
|
32
|
+
|
|
33
|
+
@lock: Thread::Mutex
|
|
34
|
+
|
|
35
|
+
@clock: ^() -> Float
|
|
36
|
+
|
|
37
|
+
@heartbeat: Integer | Float
|
|
38
|
+
|
|
39
|
+
@interval: Integer | Float
|
|
40
|
+
|
|
41
|
+
@io: IO | StringIO
|
|
42
|
+
|
|
43
|
+
# Writes to `io`: news at most every `interval` seconds, and that it is still going after `heartbeat` seconds of quiet
|
|
44
|
+
# : (?IO | StringIO, ?interval: Integer | Float, ?heartbeat: Integer | Float, ?clock: ^() -> Float) -> void
|
|
45
|
+
def initialize: (?IO | StringIO, ?interval: Integer | Float, ?heartbeat: Integer | Float, ?clock: ^() -> Float) -> void
|
|
46
|
+
|
|
47
|
+
# Takes one batch, see Log#add_output
|
|
48
|
+
# : (Batch, Log) -> void
|
|
49
|
+
def call: (Batch, Log) -> void
|
|
50
|
+
|
|
51
|
+
# Says what is still pending, and stops
|
|
52
|
+
# : () -> void
|
|
53
|
+
def close: () -> void
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# : () -> void
|
|
58
|
+
def run: () -> void
|
|
59
|
+
|
|
60
|
+
# : (?flush: bool) -> void
|
|
61
|
+
def close_locked: (?flush: bool) -> void
|
|
62
|
+
|
|
63
|
+
# : () -> void
|
|
64
|
+
def say_pending: () -> void
|
|
65
|
+
|
|
66
|
+
# : (State, String) -> void
|
|
67
|
+
def say: (State, String) -> void
|
|
68
|
+
|
|
69
|
+
ENDINGS: Array[Symbol]
|
|
70
|
+
|
|
71
|
+
PUT_ENDINGS: Hash[Symbol, Symbol]
|
|
72
|
+
|
|
73
|
+
# A squashed batch - from BufferedOutput, a stream, a mount catching up - says a task ended by
|
|
74
|
+
# putting it whole, finished. Said once, like the ending it stands for
|
|
75
|
+
# : (Event) -> Event?
|
|
76
|
+
def ending_put: (Event) -> Event?
|
|
77
|
+
|
|
78
|
+
# : (State, Event) -> String
|
|
79
|
+
def ending: (State, Event) -> String
|
|
80
|
+
|
|
81
|
+
# The task's name, after the names of every task it is under
|
|
82
|
+
# : (State, path) -> String
|
|
83
|
+
def names: (State, path) -> String
|
|
84
|
+
|
|
85
|
+
# : (Node) -> String
|
|
86
|
+
def count: (Node) -> String
|
|
87
|
+
|
|
88
|
+
# A task that reported nothing before it finished started when it finished, as far as the
|
|
89
|
+
# tree knows - so it took as long as it was open
|
|
90
|
+
# : (Node?) -> String
|
|
91
|
+
def took: (Node?) -> String
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated from lib/task_progress/fold.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Siblings drawn as one row, see below
|
|
5
|
+
class Fold < Data
|
|
6
|
+
attr_reader status(): Symbol
|
|
7
|
+
|
|
8
|
+
attr_reader count(): Integer
|
|
9
|
+
|
|
10
|
+
def self.new: (Symbol status, Integer count) -> instance
|
|
11
|
+
| (status: Symbol, count: Integer) -> instance
|
|
12
|
+
|
|
13
|
+
def self.members: () -> [ :status, :count ]
|
|
14
|
+
|
|
15
|
+
def members: () -> [ :status, :count ]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Siblings drawn as one row - "✓ 9,812 more done", "· 340 more waiting" - so a task with ten
|
|
19
|
+
# thousand children draws as a handful of rows. See State#rows, which makes them
|
|
20
|
+
class Fold
|
|
21
|
+
# What the row says
|
|
22
|
+
# : () -> String
|
|
23
|
+
def name: () -> String
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Generated from lib/task_progress/html_renderer.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Draws a tree as HTML - the same rows, with the same classes, as the browser client's renderers
|
|
5
|
+
# draw, so `task_tree.css` styles both and a page can move from one to the other unchanged.
|
|
6
|
+
# This is what TurboStreamOutput sends, and what TurboSSE streams.
|
|
7
|
+
#
|
|
8
|
+
# Elapsed times are `<task-progress-clock>` elements (`task_progress_clock.js`), which tick on
|
|
9
|
+
# between frames; everything else is as it was when the tree was drawn. A cancelable task gets
|
|
10
|
+
# a cancel button that is a plain form, posting the task's path and `cancel_token` to
|
|
11
|
+
# `cancel_url` - so it needs no JavaScript, and Turbo submits it without leaving the page.
|
|
12
|
+
#
|
|
13
|
+
# To draw rows your own way, subclass it and override `row`, or hand TurboStreamOutput anything
|
|
14
|
+
# that answers `id` and `render(snapshot, from = [])` - say, an object that renders a partial
|
|
15
|
+
class HTMLRenderer
|
|
16
|
+
# A circle with a cross in it, drawn in the row's text color - the same in every font
|
|
17
|
+
CANCEL_ICON: String
|
|
18
|
+
|
|
19
|
+
GLYPHS: Hash[Symbol, String]
|
|
20
|
+
|
|
21
|
+
# The id of the element the tree is drawn in - what a Turbo Stream targets
|
|
22
|
+
attr_reader id: String
|
|
23
|
+
|
|
24
|
+
@clock: ^() -> Float
|
|
25
|
+
|
|
26
|
+
@cancel_token: String?
|
|
27
|
+
|
|
28
|
+
@cancel_url: String?
|
|
29
|
+
|
|
30
|
+
# Draws into an element with the id `id`. Without a `cancel_url`, no task offers a cancel
|
|
31
|
+
# button. `clock` is for tests to pin down
|
|
32
|
+
# : (id: String, ?cancel_url: String?, ?cancel_token: String?, ?clock: ^() -> Float) -> void
|
|
33
|
+
def initialize: (id: String, ?cancel_url: String?, ?cancel_token: String?, ?clock: ^() -> Float) -> void
|
|
34
|
+
|
|
35
|
+
# The whole tree in its element - or the subtree at `from`, drawn as a tree of its own
|
|
36
|
+
# : (State, ?path) -> String
|
|
37
|
+
def render: (State, ?path) -> String
|
|
38
|
+
|
|
39
|
+
# One task's row, `depth` levels in
|
|
40
|
+
# : (State, path, Node, ?depth: Integer) -> String
|
|
41
|
+
def row: (State, path, Node, ?depth: Integer) -> String
|
|
42
|
+
|
|
43
|
+
# Siblings folded into one row, see State#rows - drawn as a row like any other, so the
|
|
44
|
+
# columns line up
|
|
45
|
+
# : (path, Fold, ?depth: Integer) -> String
|
|
46
|
+
def fold: (path, Fold, ?depth: Integer) -> String
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# : (String?) -> String
|
|
51
|
+
def h: (String?) -> String
|
|
52
|
+
|
|
53
|
+
# : (Node) -> String
|
|
54
|
+
def message: (Node) -> String
|
|
55
|
+
|
|
56
|
+
# The rate over the run so far, and the time left at it
|
|
57
|
+
# : (Node) -> String?
|
|
58
|
+
def pace: (Node) -> String?
|
|
59
|
+
|
|
60
|
+
# : (path, Node, Integer, bool) -> String
|
|
61
|
+
def measure: (path, Node, Integer, bool) -> String
|
|
62
|
+
|
|
63
|
+
# Ticks on in the browser between frames, from the time the tree was drawn
|
|
64
|
+
# : (Node) -> String
|
|
65
|
+
def clock: (Node) -> String
|
|
66
|
+
|
|
67
|
+
# The cancel column is there on every row, so the bars line up
|
|
68
|
+
# : (path, Node) -> String
|
|
69
|
+
def cancel: (path, Node) -> String
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated from lib/task_progress/journal.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# The changes one transaction made, so that it can be undone whole
|
|
5
|
+
class Journal
|
|
6
|
+
@saved: Hash[path, [ Node, Integer ]?]
|
|
7
|
+
|
|
8
|
+
# : () -> void
|
|
9
|
+
def initialize: () -> void
|
|
10
|
+
|
|
11
|
+
# Remembers the node as it was before the transaction first touched it - nil for one it opened.
|
|
12
|
+
# A shallow copy, and how many children it had: a transaction only ever adds children at the
|
|
13
|
+
# end, or swaps the list for a new one, so that is enough to put the list back - and a parent
|
|
14
|
+
# of a hundred thousand tasks is not copied whole every time one of them moves
|
|
15
|
+
# : (path, Node?) -> void
|
|
16
|
+
def touch: (path, Node?) -> void
|
|
17
|
+
|
|
18
|
+
# Every path it touched, opened or removed
|
|
19
|
+
# : () -> Array[path]
|
|
20
|
+
def paths: () -> Array[path]
|
|
21
|
+
|
|
22
|
+
# : (Hash[path, Node]) -> void
|
|
23
|
+
def rollback: (Hash[path, Node]) -> void
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Generated from lib/task_progress/log.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# The sequencer. Every change to a tree goes through `transaction`, which applies its events
|
|
5
|
+
# under the one lock and commits them as a numbered Batch - so the lock is what puts the
|
|
6
|
+
# changes from many threads into one order, and a batch is what crosses a process, a Ractor
|
|
7
|
+
# or the wire as one piece.
|
|
8
|
+
#
|
|
9
|
+
# Outputs get the batches strictly in order, but outside that lock: a worker never waits on
|
|
10
|
+
# another's output while it holds the tree. Delivery is synchronous to whoever committed, so
|
|
11
|
+
# a slow output slows its committer down - wrap it in BufferedOutput when it must not
|
|
12
|
+
class Log
|
|
13
|
+
# The id its batches carry - ten lower case characters, unless given
|
|
14
|
+
attr_reader id: String
|
|
15
|
+
|
|
16
|
+
# The seq of the last batch it committed
|
|
17
|
+
attr_reader seq: Integer
|
|
18
|
+
|
|
19
|
+
# The stream key the tree is written to or read from, when it is - see TaskProgress.handoff
|
|
20
|
+
attr_reader key: String?
|
|
21
|
+
|
|
22
|
+
# Lower case only, like the task ids, so neither can clash with another on a filesystem that
|
|
23
|
+
# ignores case (macOS, Windows) should one ever end up in a file name
|
|
24
|
+
ID_CHARS: Array[String]
|
|
25
|
+
|
|
26
|
+
@clock: ^() -> Float
|
|
27
|
+
|
|
28
|
+
@random: Random
|
|
29
|
+
|
|
30
|
+
@lock: Thread::Mutex
|
|
31
|
+
|
|
32
|
+
@delivery: Thread::Mutex
|
|
33
|
+
|
|
34
|
+
@state: State
|
|
35
|
+
|
|
36
|
+
@outputs: Array[output]
|
|
37
|
+
|
|
38
|
+
@outbox: Array[[ Batch, Array[output] ]]
|
|
39
|
+
|
|
40
|
+
@snapshot: State?
|
|
41
|
+
|
|
42
|
+
@touched: Set[path]
|
|
43
|
+
|
|
44
|
+
@on_cancel: Hash[path, Array[^(String?) -> void]]
|
|
45
|
+
|
|
46
|
+
@on_error: ^(Exception, untyped) -> void
|
|
47
|
+
|
|
48
|
+
@changed: Thread::ConditionVariable
|
|
49
|
+
|
|
50
|
+
@closers: Array[^() -> void]
|
|
51
|
+
|
|
52
|
+
# An empty log. `clock` gives the time to stamp batches with, and `random` draws task ids - both for tests to pin down
|
|
53
|
+
# : (?id: String | Symbol, ?key: String?, ?clock: ^() -> Float, ?random: Random) -> void
|
|
54
|
+
def initialize: (?id: String | Symbol, ?key: String?, ?clock: ^() -> Float, ?random: Random) -> void
|
|
55
|
+
|
|
56
|
+
# Hears about an output that raised, with the error and the output; it warns by default
|
|
57
|
+
attr_accessor on_error: ^(Exception, untyped) -> void
|
|
58
|
+
|
|
59
|
+
# Runs the block under the lock, collecting what it adds with `tx.<<`; it commits as one
|
|
60
|
+
# batch, or not at all if the block raises. A transaction opened inside another one on the
|
|
61
|
+
# same fiber joins it. The block holds the lock throughout - keep it short, and never wait
|
|
62
|
+
# in it on another thread that writes to this log. `at` is the time it happens at - now,
|
|
63
|
+
# unless it relays what happened elsewhere, earlier
|
|
64
|
+
# : [T] (?at: Float?) { (Transaction) -> T } -> T
|
|
65
|
+
def transaction: [T] (?at: Float?) { (Transaction) -> T } -> T
|
|
66
|
+
|
|
67
|
+
# Commits the events as one batch
|
|
68
|
+
# : (*Event) -> void
|
|
69
|
+
def append: (*Event) -> void
|
|
70
|
+
|
|
71
|
+
# An output is anything with `call(batch, log)`. Unless `replay: false`, it first gets the
|
|
72
|
+
# tree so far as one squashed batch, `prev` 0, and then every batch after it. With a `path`
|
|
73
|
+
# it gets only that subtree, as a tree of its own - see Branch
|
|
74
|
+
# : (?output?, ?replay: bool, ?path: path) ?{ (Batch, Log) -> void } -> output
|
|
75
|
+
def add_output: (?output?, ?replay: bool, ?path: path) ?{ (Batch, Log) -> void } -> output
|
|
76
|
+
|
|
77
|
+
# Takes the output as it was given to `add_output`. It hears nothing more once this returns -
|
|
78
|
+
# save a batch being delivered to it on another thread just then
|
|
79
|
+
# : (output) -> void
|
|
80
|
+
def remove_output: (output) -> void
|
|
81
|
+
|
|
82
|
+
# Calls the block with the reason once the task at `path` is canceled - at once, if it already
|
|
83
|
+
# was. A task that ends any other way drops it. The block runs on whichever thread delivers
|
|
84
|
+
# the cancel, after it has been committed: keep it short, and let it unblock the work (close a
|
|
85
|
+
# connection, raise into a thread) rather than do the work
|
|
86
|
+
# : (path) { (String?) -> void } -> void
|
|
87
|
+
def on_cancel: (path) { (String?) -> void } -> void
|
|
88
|
+
|
|
89
|
+
# A frozen copy of the tree, safe to read from anywhere - and from inside an output too,
|
|
90
|
+
# since outputs run outside the lock
|
|
91
|
+
# : () -> State
|
|
92
|
+
def snapshot: () -> State
|
|
93
|
+
|
|
94
|
+
# The tree so far as one batch of puts - what a late reader starts from. With a `path`, the
|
|
95
|
+
# subtree there, rooted at `[]` the way a Branch would send it
|
|
96
|
+
# : (?path) -> Batch
|
|
97
|
+
def squashed: (?path) -> Batch
|
|
98
|
+
|
|
99
|
+
# Waits up to `timeout` seconds for a batch after `seq` to commit, and returns the seq then
|
|
100
|
+
# : (Integer, Integer | Float) -> Integer
|
|
101
|
+
def wait_for_change: (Integer, Integer | Float) -> Integer
|
|
102
|
+
|
|
103
|
+
# Runs the block when the log is closed - to stop a thread that feeds it, or flush an output
|
|
104
|
+
# : () { () -> void } -> void
|
|
105
|
+
def on_close: () { () -> void } -> void
|
|
106
|
+
|
|
107
|
+
# Stops what was started for the tree - following a stream, beating, buffering - for when
|
|
108
|
+
# nobody needs it any more. The tree itself stays as it is
|
|
109
|
+
# : () -> void
|
|
110
|
+
def close: () -> void
|
|
111
|
+
|
|
112
|
+
# Its id and seq
|
|
113
|
+
# : () -> String
|
|
114
|
+
def inspect: () -> String
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# : () -> Transaction?
|
|
119
|
+
def current: () -> Transaction?
|
|
120
|
+
|
|
121
|
+
# Thread#[] is fiber-local and, unlike Fiber storage, a new thread or fiber does not inherit
|
|
122
|
+
# it - which is what we want, since only the fiber that took the lock may join
|
|
123
|
+
# : (Transaction?) -> void
|
|
124
|
+
def current=: (Transaction?) -> void
|
|
125
|
+
|
|
126
|
+
# : (Batch) -> void
|
|
127
|
+
def call_cancel_callbacks: (Batch) -> void
|
|
128
|
+
|
|
129
|
+
# : (Transaction) -> void
|
|
130
|
+
def commit_locked: (Transaction) -> void
|
|
131
|
+
|
|
132
|
+
# One deliverer at a time, taking the outbox in order. A commit made from inside an output
|
|
133
|
+
# finds its own fiber already delivering, and leaves its batch for the loop it is inside of
|
|
134
|
+
#
|
|
135
|
+
# Each batch goes to all its outputs with interrupts held off, so one can never reach some
|
|
136
|
+
# outputs and not others. An interrupt waits for the batch in hand, not for the whole outbox
|
|
137
|
+
# : () -> void
|
|
138
|
+
def deliver: () -> void
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated from lib/task_progress/memory_cache.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A store kept in this process, for where `Rails.cache` is not to hand - tests, a Rack app that
|
|
5
|
+
# runs its jobs in-process. Anything with `read`, `write` and `delete` will do as a store; this
|
|
6
|
+
# is the smallest one, and is safe to share between threads
|
|
7
|
+
class MemoryCache
|
|
8
|
+
@entries: Hash[String, untyped]
|
|
9
|
+
|
|
10
|
+
@lock: Thread::Mutex
|
|
11
|
+
|
|
12
|
+
# An empty store
|
|
13
|
+
# : () -> void
|
|
14
|
+
def initialize: () -> void
|
|
15
|
+
|
|
16
|
+
# What was written under `key`, or nil
|
|
17
|
+
# : (String) -> untyped
|
|
18
|
+
def read: (String) -> untyped
|
|
19
|
+
|
|
20
|
+
# Keeps `value` under `key`; options, such as `expires_in:`, are taken and ignored
|
|
21
|
+
# : (String, untyped, **untyped) -> void
|
|
22
|
+
def write: (String, untyped, **untyped) -> void
|
|
23
|
+
|
|
24
|
+
# Forgets what is under `key`
|
|
25
|
+
# : (String) -> void
|
|
26
|
+
def delete: (String) -> void
|
|
27
|
+
end
|
|
28
|
+
end
|