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,40 @@
|
|
|
1
|
+
# Generated from lib/task_progress/trees_app.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A Rack app that serves handed-off trees to pages, by a Token rather than by the tree's key -
|
|
5
|
+
# what TaskProgress.for_page hands out links to. The Railtie mounts it at `/task_progress/trees`.
|
|
6
|
+
#
|
|
7
|
+
# GET /:token/events Server-Sent Events, see SSE
|
|
8
|
+
# GET /:token/poll the tree, polled or long-polled, see Poll
|
|
9
|
+
# POST /:token/cancellations path=x3k0-00af (a form, or JSON)
|
|
10
|
+
# 202 {"path": ..., "state": "requested"} asked of whoever works on it
|
|
11
|
+
# 200 {"path": ..., "state": "moot"} nothing left to cancel - no such task, or it ended
|
|
12
|
+
# 409 the task is not cancelable
|
|
13
|
+
# 403 a token that is forged, mangled or expired
|
|
14
|
+
# 404 a tree that is gone, or anything else
|
|
15
|
+
#
|
|
16
|
+
# Issuing the token is the authorization: it happens in an action of yours, which has decided
|
|
17
|
+
# the page may see that tree. The key itself never leaves the server
|
|
18
|
+
class TreesApp
|
|
19
|
+
@tokens: Token
|
|
20
|
+
|
|
21
|
+
@stream_store: untyped
|
|
22
|
+
|
|
23
|
+
# Checks tokens with `tokens`, and reads trees from `stream_store` - TaskProgress.stream_store
|
|
24
|
+
# unless given
|
|
25
|
+
# : (tokens: Token, ?stream_store: untyped) -> void
|
|
26
|
+
def initialize: (tokens: Token, ?stream_store: untyped) -> void
|
|
27
|
+
|
|
28
|
+
# Serves one request
|
|
29
|
+
# : (Hash[String, untyped]) -> [Integer, Hash[String, String], untyped]
|
|
30
|
+
def call: (Hash[String, untyped]) -> [ Integer, Hash[String, String], untyped ]
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# : (String, Hash[String, untyped], untyped) -> [Integer, Hash[String, String], Array[String]]
|
|
35
|
+
def cancel: (String, Hash[String, untyped], untyped) -> [ Integer, Hash[String, String], Array[String] ]
|
|
36
|
+
|
|
37
|
+
# : (Integer, Hash[Symbol, untyped]) -> [Integer, Hash[String, String], Array[String]]
|
|
38
|
+
def respond: (Integer, Hash[Symbol, untyped]) -> [ Integer, Hash[String, String], Array[String] ]
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Generated from lib/task_progress/turbo_helper.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# View helpers, in every view once the gem is in a Rails app
|
|
5
|
+
module TurboHelper
|
|
6
|
+
# The tree a job broadcasts with `TaskProgress.broadcast(record, ...)`, kept up to date: drawn
|
|
7
|
+
# as it stood at its last frame, and subscribed to every frame after that. Needs turbo-rails,
|
|
8
|
+
# and `task_tree.css` on the page - plus `task_progress_clock.js`, for elapsed times to tick
|
|
9
|
+
#
|
|
10
|
+
# <%= task_progress_tree @csv_import %>
|
|
11
|
+
# : (untyped, ?store: untyped) -> String
|
|
12
|
+
def task_progress_tree: (untyped, ?store: untyped) -> String
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Generated from lib/task_progress/turbo_sse.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A Rack body streaming a tree as Turbo Streams over Server-Sent Events, for a page that shows
|
|
5
|
+
# it with `<turbo-stream-source src="...">` - Turbo with no Action Cable, and no JavaScript of
|
|
6
|
+
# your own. Every frame replaces the whole tree by morphing, at most every `interval` seconds
|
|
7
|
+
# however fast the tree changes; a reconnect starts with the whole tree, so nothing is ever
|
|
8
|
+
# missed for good. Given a subtask, it streams that branch alone.
|
|
9
|
+
#
|
|
10
|
+
# TaskProgress::TurboSSE.response(task, renderer:, source: "progress-source")
|
|
11
|
+
#
|
|
12
|
+
# Once the task finishes it sends the last frame, removes the element with the id `source` -
|
|
13
|
+
# the `<turbo-stream-source>`, so the browser does not reconnect to a finished stream - and ends.
|
|
14
|
+
# Without a `source` the browser reconnects every `reconnect` seconds, and gets the last frame
|
|
15
|
+
# each time. Given a key, it follows the tree handed off there, as SSE does
|
|
16
|
+
class TurboSSE
|
|
17
|
+
# What a Turbo SSE response is sent with - the same as an SSE one
|
|
18
|
+
HEADERS: Hash[String, String]
|
|
19
|
+
|
|
20
|
+
@close: bool
|
|
21
|
+
|
|
22
|
+
@reconnect: Integer | Float
|
|
23
|
+
|
|
24
|
+
@source: String?
|
|
25
|
+
|
|
26
|
+
@heartbeat: Integer | Float
|
|
27
|
+
|
|
28
|
+
@interval: Integer | Float
|
|
29
|
+
|
|
30
|
+
@renderer: TurboStreamOutput::_Renderer
|
|
31
|
+
|
|
32
|
+
@task: TaskProgress
|
|
33
|
+
|
|
34
|
+
# A whole Rack response: status, headers and this body - or a 404, for a key with nothing at it
|
|
35
|
+
# : (TaskProgress | String, **untyped) -> [Integer, Hash[String, String], TurboSSE | Array[String]]
|
|
36
|
+
def self.response: (TaskProgress | String, **untyped) -> [ Integer, Hash[String, String], TurboSSE | Array[String] ]
|
|
37
|
+
|
|
38
|
+
# Streams `task`, drawn by `renderer` - an HTMLRenderer, or anything that answers `id` and
|
|
39
|
+
# `render(snapshot, from)`, `from` being the task's path. `source` is the id of the
|
|
40
|
+
# `<turbo-stream-source>` to remove at the end
|
|
41
|
+
# : (TaskProgress | String, renderer: TurboStreamOutput::_Renderer, ?stream_store: untyped, ?interval: Integer | Float, ?heartbeat: Integer | Float, ?reconnect: Integer | Float, ?source: String?) -> void
|
|
42
|
+
def initialize: (TaskProgress | String, renderer: TurboStreamOutput::_Renderer, ?stream_store: untyped, ?interval: Integer | Float, ?heartbeat: Integer | Float, ?reconnect: Integer | Float, ?source: String?) -> void
|
|
43
|
+
|
|
44
|
+
# Yields the stream a chunk at a time, until the task finishes - as a Rack body does
|
|
45
|
+
# : () { (String) -> void } -> void
|
|
46
|
+
def each: () { (String) -> void } -> void
|
|
47
|
+
|
|
48
|
+
# Stops following a tree it followed by its key - Rack calls it when the response is over
|
|
49
|
+
# : () -> void
|
|
50
|
+
def close: () -> void
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Finished, or pruned away - which is as finished as it will ever be
|
|
55
|
+
# : () -> bool
|
|
56
|
+
def ended?: () -> bool
|
|
57
|
+
|
|
58
|
+
# : () -> String
|
|
59
|
+
def frame: () -> String
|
|
60
|
+
|
|
61
|
+
# : (State) -> String
|
|
62
|
+
def render: (State) -> String
|
|
63
|
+
|
|
64
|
+
# An unnamed event - what `<turbo-stream-source>` listens for. A line of SSE ends at a CR as
|
|
65
|
+
# much as at a LF, and task names and messages - curl's and ffmpeg's, say - are full of CRs
|
|
66
|
+
# : (String) -> String
|
|
67
|
+
def message: (String) -> String
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Generated from lib/task_progress/turbo_stream_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Draws the whole tree as HTML on every batch, as a Turbo Stream that replaces it in the page by
|
|
5
|
+
# morphing, and hands that to `to` - in Rails, Turbo::StreamsChannel, which reaches every
|
|
6
|
+
# page subscribed with `turbo_stream_from`, whichever process the tree lives in. Put it behind a
|
|
7
|
+
# BufferedOutput, as TaskProgress.broadcast does, and a spammy job is drawn a few times a second.
|
|
8
|
+
#
|
|
9
|
+
# The whole tree, every time: a Turbo Stream has no seq and no gaps to notice, so a page that
|
|
10
|
+
# missed a frame - or joined late - is set right by the next one. Morphing touches only what
|
|
11
|
+
# differs, so a bar's width eases rather than jumps.
|
|
12
|
+
#
|
|
13
|
+
# With a `store`, the latest frame is also written under `key`, for a page to draw the tree
|
|
14
|
+
# before the first broadcast reaches it - see TurboHelper#task_progress_tree
|
|
15
|
+
class TurboStreamOutput
|
|
16
|
+
interface _Renderer
|
|
17
|
+
def render: (State snapshot, ?path from) -> String
|
|
18
|
+
|
|
19
|
+
def id: () -> String
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
interface _Store
|
|
23
|
+
def read: (String key) -> untyped
|
|
24
|
+
|
|
25
|
+
def write: (String key, untyped value, **untyped) -> void
|
|
26
|
+
|
|
27
|
+
def delete: (String key) -> void
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The store key the latest frame is written under
|
|
31
|
+
attr_reader key: String
|
|
32
|
+
|
|
33
|
+
@renderer: _Renderer
|
|
34
|
+
|
|
35
|
+
@to: ^(String) -> void
|
|
36
|
+
|
|
37
|
+
@store: _Store?
|
|
38
|
+
|
|
39
|
+
# Draws with `renderer`, and hands each frame - a `<turbo-stream>` element - to `to`
|
|
40
|
+
# : (renderer: _Renderer, to: ^(String) -> void, ?store: _Store?, ?key: String) -> void
|
|
41
|
+
def initialize: (renderer: _Renderer, to: ^(String) -> void, ?store: _Store?, ?key: String) -> void
|
|
42
|
+
|
|
43
|
+
# Draws the tree as it stands - the batch only says it changed
|
|
44
|
+
# : (Batch, Log) -> void
|
|
45
|
+
def call: (Batch, Log) -> void
|
|
46
|
+
|
|
47
|
+
# A Turbo Stream that replaces the element `target` with `html`, morphing it
|
|
48
|
+
# : (String, String) -> String
|
|
49
|
+
def self.frame: (String, String) -> String
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Generated from lib/task_progress/wire.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A batch as JSON, one object per line:
|
|
5
|
+
#
|
|
6
|
+
# {"v":1,"log":"k7p2qd9z4m","prev":11,"seq":12,"at":1730000000.123,"ops":[[["x3k0","00af"],"set",40],[["x3k0"],"note","Reading"]]}
|
|
7
|
+
#
|
|
8
|
+
# The reading side is meant for a writer we do not trust - a subprocess parsing hostile input,
|
|
9
|
+
# a stream relayed from elsewhere - so nothing it sends is taken at its word: a batch with
|
|
10
|
+
# anything wrong in it is dropped whole, numbers are clamped, strings scrubbed and cut, paths
|
|
11
|
+
# bounded, and nothing in an error is ever turned into a class
|
|
12
|
+
module Wire
|
|
13
|
+
# The version of the format every batch carries as `v`. A reader takes only its own: a batch
|
|
14
|
+
# from a newer writer is dropped, as a garbled one would be, rather than half understood
|
|
15
|
+
VERSION: Integer
|
|
16
|
+
|
|
17
|
+
# The longest line a reader takes - see the MANUAL for what every limit is for
|
|
18
|
+
LINE_LIMIT: Integer
|
|
19
|
+
|
|
20
|
+
# Where a string is cut
|
|
21
|
+
TEXT_LIMIT: Integer
|
|
22
|
+
|
|
23
|
+
# How much of a backtrace is kept
|
|
24
|
+
BACKTRACE_LINES: Integer
|
|
25
|
+
|
|
26
|
+
# How many causes of an error are kept
|
|
27
|
+
CAUSES: Integer
|
|
28
|
+
|
|
29
|
+
# How deep a path may go
|
|
30
|
+
MAX_DEPTH: Integer
|
|
31
|
+
|
|
32
|
+
# What a task id may be: lower case letters and digits, 16 at most
|
|
33
|
+
SEGMENT: Regexp
|
|
34
|
+
|
|
35
|
+
# The most events one batch may carry
|
|
36
|
+
MAX_OPS: Integer
|
|
37
|
+
|
|
38
|
+
# The largest count or total - what a JavaScript number holds exactly
|
|
39
|
+
MAX_COUNT: Integer
|
|
40
|
+
|
|
41
|
+
# What a `put` may say a task's status is
|
|
42
|
+
STATUSES: Array[String]
|
|
43
|
+
|
|
44
|
+
# The batch as a JSON-ready Hash
|
|
45
|
+
# : (Batch) -> Hash[Symbol, untyped]
|
|
46
|
+
def self.to_h: (Batch) -> Hash[Symbol, untyped]
|
|
47
|
+
|
|
48
|
+
# The batch as one line of JSON, without the newline
|
|
49
|
+
# : (Batch) -> String
|
|
50
|
+
def self.dump: (Batch) -> String
|
|
51
|
+
|
|
52
|
+
# A Batch, or nil when the line is anything but a valid one
|
|
53
|
+
# : (String) -> Batch?
|
|
54
|
+
def self.parse: (String) -> Batch?
|
|
55
|
+
|
|
56
|
+
# A Batch from its Hash form - string or symbol keys - or nil if anything in it is wrong
|
|
57
|
+
# : (untyped) -> Batch?
|
|
58
|
+
def self.from_h: (untyped) -> Batch?
|
|
59
|
+
|
|
60
|
+
# Reads batches until EOF. An overlong line is skipped, as is a garbled one, and a line
|
|
61
|
+
# cut off by a writer that died is never yielded
|
|
62
|
+
# : (IO, ?limit: Integer) { (Batch) -> void } -> void
|
|
63
|
+
def self.each_batch: (IO, ?limit: Integer) { (Batch) -> void } -> void
|
|
64
|
+
|
|
65
|
+
# An exception - its class, message, backtrace and causes - or a message, as an error Hash
|
|
66
|
+
# : (Exception | String | untyped, ?Integer) -> Hash[Symbol, untyped]
|
|
67
|
+
def self.error_to_h: (Exception | String | untyped, ?Integer) -> Hash[Symbol, untyped]
|
|
68
|
+
|
|
69
|
+
# Every string that leaves or enters goes through here: valid UTF-8, and cut
|
|
70
|
+
# : (untyped) -> String?
|
|
71
|
+
def self.text: (untyped) -> String?
|
|
72
|
+
|
|
73
|
+
# : (untyped) -> Event?
|
|
74
|
+
private def self.event: (untyped) -> Event?
|
|
75
|
+
|
|
76
|
+
# : (untyped) -> path?
|
|
77
|
+
private def self.path: (untyped) -> path?
|
|
78
|
+
|
|
79
|
+
# : (Symbol, untyped) -> untyped
|
|
80
|
+
private def self.value: (Symbol, untyped) -> untyped
|
|
81
|
+
|
|
82
|
+
# : (untyped, ?stale: bool) -> (Hash[Symbol, untyped] | :invalid)
|
|
83
|
+
private def self.attributes: (untyped, ?stale: bool) -> (Hash[Symbol, untyped] | :invalid)
|
|
84
|
+
|
|
85
|
+
# : (untyped) -> (Hash[Symbol, untyped] | :invalid)
|
|
86
|
+
private def self.node: (untyped) -> (Hash[Symbol, untyped] | :invalid)
|
|
87
|
+
|
|
88
|
+
# : (untyped) -> Integer?
|
|
89
|
+
private def self.count: (untyped) -> Integer?
|
|
90
|
+
|
|
91
|
+
# : (untyped) -> Float?
|
|
92
|
+
private def self.time: (untyped) -> Float?
|
|
93
|
+
|
|
94
|
+
# Finite integers and floats only - not a Rational, nor a Complex that has no Float to give,
|
|
95
|
+
# nor the Infinity that `1e999` parses to, which no JSON could ever be written with again
|
|
96
|
+
# : (untyped) -> Float?
|
|
97
|
+
private def self.number: (untyped) -> Float?
|
|
98
|
+
|
|
99
|
+
# : (Hash[untyped, untyped], ?Integer) -> Hash[Symbol, untyped]
|
|
100
|
+
private def self.error: (Hash[untyped, untyped], ?Integer) -> Hash[Symbol, untyped]
|
|
101
|
+
end
|
|
102
|
+
end
|