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,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,6 @@
1
+ # Generated from lib/task_progress/version.rb with RBS::Inline
2
+
3
+ class TaskProgress
4
+ # The gem's version
5
+ VERSION: String
6
+ 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