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,83 @@
|
|
|
1
|
+
# Generated from lib/task_progress/memory_stream_store.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A stream store kept in this process - a stream per tree, each under its own key: what
|
|
5
|
+
# TaskProgress.stream_store is until told otherwise, and what the tests and the bench run on.
|
|
6
|
+
# It does what RedisStreamStore does - append-only entries under a
|
|
7
|
+
# key, read from an id on and waited for, trimmed from the front, plus plain values that
|
|
8
|
+
# expire - so a tree handed off here behaves as one handed off through Redis, as long as the
|
|
9
|
+
# job runs in the same process.
|
|
10
|
+
#
|
|
11
|
+
# Ids are "n-0", like Redis's, and grow across all keys. Safe to share between threads.
|
|
12
|
+
#
|
|
13
|
+
# `set`, `trim`, `delete` and `expire` take a `guard:` - a key and the value it must hold - and
|
|
14
|
+
# do nothing, returning false, unless it does. Checked and done in one step, which is what keeps
|
|
15
|
+
# a writer another has replaced from writing over it, see StreamOutput
|
|
16
|
+
class MemoryStreamStore
|
|
17
|
+
@namespace: String?
|
|
18
|
+
|
|
19
|
+
@appended: Thread::ConditionVariable
|
|
20
|
+
|
|
21
|
+
@lock: Thread::Mutex
|
|
22
|
+
|
|
23
|
+
@counter: Integer
|
|
24
|
+
|
|
25
|
+
@expiries: Hash[String, Float]
|
|
26
|
+
|
|
27
|
+
@values: Hash[String, String]
|
|
28
|
+
|
|
29
|
+
@entries: Hash[String, Array[[ Integer, String ]]]
|
|
30
|
+
|
|
31
|
+
# An empty store. With a `namespace`, its keys go under it - see RedisStreamStore
|
|
32
|
+
# : (?namespace: String?) -> void
|
|
33
|
+
def initialize: (?namespace: String?) -> void
|
|
34
|
+
|
|
35
|
+
# Adds `data` at the end of the stream at `key`, and returns its id
|
|
36
|
+
# : (String, String) -> String
|
|
37
|
+
def append: (String, String) -> String
|
|
38
|
+
|
|
39
|
+
# The entries after the id `after` - all of them when it is nil - as `[id, data]` pairs. With
|
|
40
|
+
# a `timeout`, waits up to that many seconds for one to come when there are none yet
|
|
41
|
+
# : (String, ?after: String?, ?timeout: (Integer | Float)?) -> Array[[String, String]]
|
|
42
|
+
def read: (String, ?after: String?, ?timeout: (Integer | Float)?) -> Array[[ String, String ]]
|
|
43
|
+
|
|
44
|
+
# The value at `key`, or nil
|
|
45
|
+
# : (String) -> String?
|
|
46
|
+
def get: (String) -> String?
|
|
47
|
+
|
|
48
|
+
# Keeps `value` at `key`, for `expires_in` seconds if given
|
|
49
|
+
# : (String, String, ?expires_in: (Integer | Float)?, ?guard: [String, String]?) -> bool
|
|
50
|
+
def set: (String, String, ?expires_in: (Integer | Float)?, ?guard: [ String, String ]?) -> bool
|
|
51
|
+
|
|
52
|
+
# Forgets the streams and values at these keys
|
|
53
|
+
# : (*String, ?guard: [String, String]?) -> bool
|
|
54
|
+
def delete: (*String, ?guard: [ String, String ]?) -> bool
|
|
55
|
+
|
|
56
|
+
# Drops the entries before the id `before`
|
|
57
|
+
# : (String, before: String, ?guard: [String, String]?) -> bool
|
|
58
|
+
def trim: (String, before: String, ?guard: [ String, String ]?) -> bool
|
|
59
|
+
|
|
60
|
+
# Forgets what is at `key` - a stream or a value - `seconds` from now
|
|
61
|
+
# : (String, Integer | Float, ?guard: [String, String]?) -> bool
|
|
62
|
+
def expire: (String, Integer | Float, ?guard: [ String, String ]?) -> bool
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Runs the block under the lock if the guard holds, and says whether it did
|
|
67
|
+
# : ([String, String]?) { () -> void } -> bool
|
|
68
|
+
def guarded: ([ String, String ]?) { () -> void } -> bool
|
|
69
|
+
|
|
70
|
+
# : (String) -> String
|
|
71
|
+
def ns: (String) -> String
|
|
72
|
+
|
|
73
|
+
# : () -> Float
|
|
74
|
+
def now: () -> Float
|
|
75
|
+
|
|
76
|
+
# Always nil, so a reader can `||` past it
|
|
77
|
+
# : (String) -> nil
|
|
78
|
+
def expire_locked: (String) -> nil
|
|
79
|
+
|
|
80
|
+
# : (String) -> void
|
|
81
|
+
def forget: (String) -> void
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Generated from lib/task_progress/mount.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Grafts a tree that lives elsewhere under one node of this one. The remote root lands on the
|
|
5
|
+
# mount node itself and its `["x3k0"]` on `[*mount, "x3k0"]`, so mounting is prefixing, and
|
|
6
|
+
# a relay through several processes prefixes once per hop.
|
|
7
|
+
#
|
|
8
|
+
# The mount node is sealed: only its Mount writes under it, so a remote can never touch a
|
|
9
|
+
# node outside its prefix - nor rename or reweigh the mount, which stay the parent's to set.
|
|
10
|
+
# Each remote batch is re-committed here as one batch, or dropped whole if it does not apply.
|
|
11
|
+
# A batch that does not follow the last one (a gap) marks the subtree stale, and it stays so
|
|
12
|
+
# until a batch with `prev` 0 replaces it whole.
|
|
13
|
+
#
|
|
14
|
+
# Fed from a stream (see #follow_stream) it goes by the latest writer instead: a batch with
|
|
15
|
+
# `prev` 0 from a log it has not seen is a new run - a job retried, a worker that took over -
|
|
16
|
+
# and replaces the subtree whole, even one that had finished, while what the old writer still
|
|
17
|
+
# sends is ignored
|
|
18
|
+
class Mount
|
|
19
|
+
# The log the mount node lives in
|
|
20
|
+
attr_reader log: Log
|
|
21
|
+
|
|
22
|
+
# The mount node
|
|
23
|
+
attr_reader path: path
|
|
24
|
+
|
|
25
|
+
@control: IO?
|
|
26
|
+
|
|
27
|
+
@last_seq: Integer?
|
|
28
|
+
|
|
29
|
+
@control_lock: Thread::Mutex
|
|
30
|
+
|
|
31
|
+
@pid_lock: Thread::Mutex
|
|
32
|
+
|
|
33
|
+
@source: String?
|
|
34
|
+
|
|
35
|
+
@stale: bool
|
|
36
|
+
|
|
37
|
+
@remote_ended: bool
|
|
38
|
+
|
|
39
|
+
@controlled: bool
|
|
40
|
+
|
|
41
|
+
@reaped: bool
|
|
42
|
+
|
|
43
|
+
@own: bool
|
|
44
|
+
|
|
45
|
+
@following: bool
|
|
46
|
+
|
|
47
|
+
@forward: (^(String) -> void)?
|
|
48
|
+
|
|
49
|
+
@reader: StreamReader?
|
|
50
|
+
|
|
51
|
+
@follower: Thread?
|
|
52
|
+
|
|
53
|
+
@stopping: bool
|
|
54
|
+
|
|
55
|
+
@silent: bool
|
|
56
|
+
|
|
57
|
+
@gave_up: bool
|
|
58
|
+
|
|
59
|
+
@generation: Integer
|
|
60
|
+
|
|
61
|
+
# For the node at `path` - see TaskProgress#mount, which opens one. Unless `own`, the
|
|
62
|
+
# remote's root names and weighs the mount node too - for a tree that is nothing but a mount,
|
|
63
|
+
# as TaskProgress.follow makes
|
|
64
|
+
# : (Log, path, ?own: bool) -> void
|
|
65
|
+
def initialize: (Log, path, ?own: bool) -> void
|
|
66
|
+
|
|
67
|
+
# The mount node, as a task of this tree
|
|
68
|
+
# : () -> TaskProgress
|
|
69
|
+
def task: () -> TaskProgress
|
|
70
|
+
|
|
71
|
+
# Whether a batch went missing, and the subtree waits for a fresh squash
|
|
72
|
+
# : () -> bool
|
|
73
|
+
def stale?: () -> bool
|
|
74
|
+
|
|
75
|
+
# Takes a Batch, its Hash form, or a line of NDJSON. Returns :applied, :gap, :rejected, or
|
|
76
|
+
# :duplicate for one already applied - redelivery is ordinary after a reconnect or a replay.
|
|
77
|
+
# Following a stream, a batch from a writer another has since replaced is :superseded
|
|
78
|
+
# : (Batch | Hash[untyped, untyped] | String) -> Symbol
|
|
79
|
+
def <<: (Batch | Hash[untyped, untyped] | String) -> Symbol
|
|
80
|
+
|
|
81
|
+
# Reads NDJSON batches off an IO until EOF
|
|
82
|
+
# : (IO) -> self
|
|
83
|
+
def read: (IO) -> self
|
|
84
|
+
|
|
85
|
+
# Runs a command with TASK_PROGRESS_FD pointing at a pipe, follows it until it exits, and
|
|
86
|
+
# returns its Process::Status. A child that exits without finishing its root fails the mount.
|
|
87
|
+
#
|
|
88
|
+
# A second pipe, TASK_PROGRESS_CONTROL_FD, carries cancel requests the other way, so tasks in
|
|
89
|
+
# the child can be canceled from here - see #request_cancel; TaskProgress.cli reads it. When
|
|
90
|
+
# the mount itself is canceled, the child is asked to cancel its root, and is sent TERM if it
|
|
91
|
+
# is still running `grace` seconds later. Interrupted (Ctrl-C, a timeout), it terminates the
|
|
92
|
+
# child at once
|
|
93
|
+
# : (*untyped command, ?grace: Integer | Float, **untyped options) -> Process::Status?
|
|
94
|
+
def spawn: (*untyped command, ?grace: Integer | Float, **untyped options) -> Process::Status?
|
|
95
|
+
|
|
96
|
+
# Follows the tree a StreamOutput writes to `stream` at `key` - what TaskProgress.handoff and
|
|
97
|
+
# TaskProgress.adopt write - on a thread of its own, checking every `poll` seconds. Cancel
|
|
98
|
+
# requests go back to the writer through the stream, and canceling the mount asks the writer
|
|
99
|
+
# to cancel its root.
|
|
100
|
+
#
|
|
101
|
+
# It also minds whether the writer is alive. Once one has been, a writer that stops beating
|
|
102
|
+
# marks the subtree stale, and fails the mount if it stays silent for `give_up_after`
|
|
103
|
+
# seconds - unless a new run comes along later and replaces it, as a retried job does. A
|
|
104
|
+
# tree nobody has taken up yet is waiting, not silent. Following ends once the mount is done
|
|
105
|
+
# or canceled, or the log is closed - a failed one is kept an eye on, for a retry
|
|
106
|
+
#
|
|
107
|
+
# With `watch: false` it takes what is there once, and leaves catching up again to
|
|
108
|
+
# #catch_up - for a tree read once, see TaskProgress.fetch
|
|
109
|
+
# : (untyped, String, ?poll: Integer | Float, ?give_up_after: Integer | Float, ?watch: bool) -> self
|
|
110
|
+
def follow_stream: (untyped, String, ?poll: Integer | Float, ?give_up_after: Integer | Float, ?watch: bool) -> self
|
|
111
|
+
|
|
112
|
+
# Takes what the stream has for it - waiting up to `timeout` seconds when it has nothing yet -
|
|
113
|
+
# and returns how many batches it applied. #follow_stream does this over and over
|
|
114
|
+
# : (?timeout: (Integer | Float)?) -> Integer
|
|
115
|
+
def catch_up: (?timeout: (Integer | Float)?) -> Integer
|
|
116
|
+
|
|
117
|
+
# Stops following, see #follow_stream - closing the log does it
|
|
118
|
+
# : () -> void
|
|
119
|
+
def stop_following: () -> void
|
|
120
|
+
|
|
121
|
+
# Passes a cancel request on to the child, for the task at `path` in its own tree. Returns
|
|
122
|
+
# :forwarded, or :moot when there is no child to ask any more - it has exited, or this mount
|
|
123
|
+
# was never spawned. Whether the child cancels anything shows up the stream, if it does
|
|
124
|
+
# : (path, ?String?) -> (:forwarded | :moot)
|
|
125
|
+
def request_cancel: (path, ?String?) -> (:forwarded | :moot)
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
# The parent may still end the subtree - when the remote went away, say. From outside, end
|
|
130
|
+
# the mount's own task: `mount.task.done!`
|
|
131
|
+
# : () -> self
|
|
132
|
+
def done!: () -> self
|
|
133
|
+
|
|
134
|
+
# Fails the mount, and cancels what the remote left open under it
|
|
135
|
+
# : (?(Exception | String)?, **bool | Integer) -> self
|
|
136
|
+
def fail!: (?(Exception | String)?, **bool | Integer) -> self
|
|
137
|
+
|
|
138
|
+
# Cancels the mount, and what the remote left open under it
|
|
139
|
+
# : (?String?) -> self
|
|
140
|
+
def cancel!: (?String?) -> self
|
|
141
|
+
|
|
142
|
+
# Whether the mount node has ended
|
|
143
|
+
# : () -> bool
|
|
144
|
+
def finished?: () -> bool
|
|
145
|
+
|
|
146
|
+
# : (Integer | Float, Integer | Float) -> void
|
|
147
|
+
def watch: (Integer | Float, Integer | Float) -> void
|
|
148
|
+
|
|
149
|
+
# Canceling the mount asks the remote to cancel its root - the run there is now, once
|
|
150
|
+
# : () -> void
|
|
151
|
+
def arm_cancel: () -> void
|
|
152
|
+
|
|
153
|
+
# The writer beats again, and nothing went missing meanwhile
|
|
154
|
+
# : () -> void
|
|
155
|
+
def speaks_again: () -> void
|
|
156
|
+
|
|
157
|
+
# The mount node as a new run finds it: its own name and weight, nothing done yet
|
|
158
|
+
# : (State) -> Hash[Symbol, untyped]
|
|
159
|
+
def fresh: (State) -> Hash[Symbol, untyped]
|
|
160
|
+
|
|
161
|
+
# Ending the mount ends what the remote left open under it too, as Task#done! and #fail! do
|
|
162
|
+
# : (Symbol) { (Transaction) -> void } -> self
|
|
163
|
+
def finish: (Symbol) { (Transaction) -> void } -> self
|
|
164
|
+
|
|
165
|
+
# : (Batch | Hash[untyped, untyped] | String) -> Batch?
|
|
166
|
+
def coerce: (Batch | Hash[untyped, untyped] | String) -> Batch?
|
|
167
|
+
|
|
168
|
+
# In the transaction given, or one of its own
|
|
169
|
+
# : (?Transaction?) -> void
|
|
170
|
+
def mark_stale: (?Transaction?) -> void
|
|
171
|
+
|
|
172
|
+
# The remote's events, moved under the mount - or nil if one would land too deep
|
|
173
|
+
# : (Array[Event], Float) -> Array[Event]?
|
|
174
|
+
def translate: (Array[Event], Float) -> Array[Event]?
|
|
175
|
+
|
|
176
|
+
# Asks the child to cancel its root, and sends TERM if it has not exited `grace` seconds on
|
|
177
|
+
# : (Integer, Integer | Float) -> void
|
|
178
|
+
def stop: (Integer, Integer | Float) -> void
|
|
179
|
+
|
|
180
|
+
# The pid is only ever signaled while it is still ours: reaping and signaling share a lock, so
|
|
181
|
+
# a signal can never reach another process that was given the pid after ours exited
|
|
182
|
+
# : (Integer, Symbol) -> void
|
|
183
|
+
def signal: (Integer, Symbol) -> void
|
|
184
|
+
|
|
185
|
+
# : (Integer) -> Process::Status?
|
|
186
|
+
def reap: (Integer) -> Process::Status?
|
|
187
|
+
|
|
188
|
+
TIMES: Array[Symbol]
|
|
189
|
+
|
|
190
|
+
ENDINGS: Array[Symbol]
|
|
191
|
+
|
|
192
|
+
FINISHED: Array[Symbol]
|
|
193
|
+
|
|
194
|
+
# : (Hash[Symbol, untyped], Float) -> Hash[Symbol, untyped]
|
|
195
|
+
def rebased: (Hash[Symbol, untyped], Float) -> Hash[Symbol, untyped]
|
|
196
|
+
|
|
197
|
+
# : (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
198
|
+
def own_put: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated from lib/task_progress/ndjson_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# One batch per line, for a Mount at the other end of a pipe. Each line goes out in one
|
|
5
|
+
# write, so a reader never sees a batch half-written unless the writer died writing it -
|
|
6
|
+
# and Wire.each_batch drops that one
|
|
7
|
+
class NDJSONOutput
|
|
8
|
+
@io: IO | StringIO
|
|
9
|
+
|
|
10
|
+
# Writes to `io`, which it makes sync
|
|
11
|
+
# : (IO | StringIO) -> void
|
|
12
|
+
def initialize: (IO | StringIO) -> void
|
|
13
|
+
|
|
14
|
+
# Writes the batch as one line
|
|
15
|
+
# : (Batch, Log) -> void
|
|
16
|
+
def call: (Batch, Log) -> void
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Generated from lib/task_progress/node.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# One task, as the tree holds it, see below
|
|
5
|
+
class Node < Struct[String | Integer? | Integer | Integer | Float | Float | bool? | String? | Symbol | Hash[Symbol, untyped]? | Float? | Array[String] | Mount?]
|
|
6
|
+
attr_accessor name(): String
|
|
7
|
+
|
|
8
|
+
attr_accessor total(): Integer?
|
|
9
|
+
|
|
10
|
+
attr_accessor count(): Integer
|
|
11
|
+
|
|
12
|
+
attr_accessor percent(): Integer | Float
|
|
13
|
+
|
|
14
|
+
attr_accessor weight(): Float
|
|
15
|
+
|
|
16
|
+
attr_accessor expect(): Integer?
|
|
17
|
+
|
|
18
|
+
attr_accessor indeterminate(): bool?
|
|
19
|
+
|
|
20
|
+
attr_accessor unit(): String?
|
|
21
|
+
|
|
22
|
+
attr_accessor cancelable(): bool?
|
|
23
|
+
|
|
24
|
+
attr_accessor attempt(): Integer?
|
|
25
|
+
|
|
26
|
+
attr_accessor message(): String?
|
|
27
|
+
|
|
28
|
+
attr_accessor status(): Symbol
|
|
29
|
+
|
|
30
|
+
attr_accessor error(): Hash[Symbol, untyped]?
|
|
31
|
+
|
|
32
|
+
attr_accessor stale(): bool?
|
|
33
|
+
|
|
34
|
+
attr_accessor opened_at(): Float
|
|
35
|
+
|
|
36
|
+
attr_accessor started_at(): Float?
|
|
37
|
+
|
|
38
|
+
attr_accessor finished_at(): Float?
|
|
39
|
+
|
|
40
|
+
attr_accessor children(): Array[String]
|
|
41
|
+
|
|
42
|
+
attr_accessor sealed(): Mount?
|
|
43
|
+
|
|
44
|
+
attr_accessor autofinish(): bool?
|
|
45
|
+
|
|
46
|
+
def self.new: (?String name, ?Integer? total, ?Integer count, ?Integer | Float percent, ?Float weight, ?Integer? expect, ?bool? indeterminate, ?String? unit, ?bool? cancelable, ?Integer? attempt, ?String? message, ?Symbol status, ?Hash[Symbol, untyped]? error, ?bool? stale, ?Float opened_at, ?Float? started_at, ?Float? finished_at, ?Array[String] children, ?Mount? sealed, ?bool? autofinish) -> instance
|
|
47
|
+
| (?name: String, ?total: Integer?, ?count: Integer, ?percent: Integer | Float, ?weight: Float, ?expect: Integer?, ?indeterminate: bool?, ?unit: String?, ?cancelable: bool?, ?attempt: Integer?, ?message: String?, ?status: Symbol, ?error: Hash[Symbol, untyped]?, ?stale: bool?, ?opened_at: Float, ?started_at: Float?, ?finished_at: Float?, ?children: Array[String], ?sealed: Mount?, ?autofinish: bool?) -> instance
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# One task, as the tree holds it - what an output or a renderer reads: its name, status, count
|
|
51
|
+
# and the rest of State::FIELDS. `children` (their ids), `sealed` (the Mount that feeds it) and
|
|
52
|
+
# `autofinish` are the tree's own bookkeeping, and never go over the wire
|
|
53
|
+
class Node
|
|
54
|
+
# Ended by a `done`, `fail` or `cancel` of its own - whatever is still open under it
|
|
55
|
+
# : () -> bool
|
|
56
|
+
def finished?: () -> bool
|
|
57
|
+
|
|
58
|
+
# As a `put` carries it: State::FIELDS, without what is at its default
|
|
59
|
+
# : () -> Hash[Symbol, untyped]
|
|
60
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
61
|
+
|
|
62
|
+
# A copy whose list of children can change without changing this one
|
|
63
|
+
# : () -> Node
|
|
64
|
+
def deep_dup: () -> Node
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Generated from lib/task_progress/poll.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# The tree over plain GETs, for where Server-Sent Events do not get through - a proxy that
|
|
5
|
+
# buffers, a host that cuts long responses, a client that is not a browser. Every answer is
|
|
6
|
+
# the whole tree, squashed into one batch with `prev` 0, and its ETag says which tree that is:
|
|
7
|
+
# a client that sends it back in `If-None-Match` gets a 304 until something changes.
|
|
8
|
+
#
|
|
9
|
+
# Asked with `?wait=N`, it holds the request up to N seconds (`max_wait` at most) for a change
|
|
10
|
+
# before saying 304 - long polling, which holds a server thread for that long, but answers
|
|
11
|
+
# the moment there is news. `followPollingAt` in `task_tree.js` does all of this.
|
|
12
|
+
#
|
|
13
|
+
# # Rack
|
|
14
|
+
# TaskProgress::Poll.response(task, env)
|
|
15
|
+
# # Rails
|
|
16
|
+
# status, headers, body = TaskProgress::Poll.response(task, request.env)
|
|
17
|
+
# render json: body.first, status:, headers:
|
|
18
|
+
#
|
|
19
|
+
# Given a key instead of a task - a tree handed off with TaskProgress.handoff - it reads that
|
|
20
|
+
# tree from the stream, following it for as long as a long poll waits, and answers 404 for a
|
|
21
|
+
# key with nothing at it
|
|
22
|
+
module Poll
|
|
23
|
+
# What a poll is answered with
|
|
24
|
+
HEADERS: Hash[String, String]
|
|
25
|
+
|
|
26
|
+
# A whole Rack response for the task's tree - its subtree, for a subtask - or for the tree
|
|
27
|
+
# handed off at a key, in `stream`
|
|
28
|
+
# : (TaskProgress | String, ?Hash[String, untyped], ?stream_store: untyped, ?max_wait: Integer | Float) -> [Integer, Hash[String, String], Array[String]]
|
|
29
|
+
def self.response: (TaskProgress | String, ?Hash[String, untyped], ?stream_store: untyped, ?max_wait: Integer | Float) -> [ Integer, Hash[String, String], Array[String] ]
|
|
30
|
+
|
|
31
|
+
# : (TaskProgress, Hash[String, untyped], Float) -> [Integer, Hash[String, String], Array[String]]
|
|
32
|
+
def self.answer: (TaskProgress, Hash[String, untyped], Float) -> [ Integer, Hash[String, String], Array[String] ]
|
|
33
|
+
|
|
34
|
+
# Which tree a squash is: the same tree, however many batches it took, gets the same tag. Times
|
|
35
|
+
# are left out - a tree read from a stream has them moved onto this clock, a little differently
|
|
36
|
+
# every read, and they never change without something else changing with them
|
|
37
|
+
# : (Batch) -> String
|
|
38
|
+
def self.etag: (Batch) -> String
|
|
39
|
+
|
|
40
|
+
TIMES: Array[Symbol]
|
|
41
|
+
|
|
42
|
+
# The tags a client already has. A proxy that compresses the answer makes the tag weak, W/"..."
|
|
43
|
+
# : (Hash[String, untyped]) -> Array[String]
|
|
44
|
+
def self.seen: (Hash[String, untyped]) -> Array[String]
|
|
45
|
+
|
|
46
|
+
# The seconds asked for in `?wait=`, up to `max_wait`
|
|
47
|
+
# : (Hash[String, untyped], Integer | Float) -> Float
|
|
48
|
+
def self.wait: (Hash[String, untyped], Integer | Float) -> Float
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated from lib/task_progress/publishing_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Hands each batch, as its JSON-ready Hash, to anything with `publish(event_name, payload)`
|
|
5
|
+
# - a Sossego::Stream, for one. A reader turns it back into a Batch with Wire.from_h, or
|
|
6
|
+
# feeds it to a Mount as it is
|
|
7
|
+
class PublishingOutput
|
|
8
|
+
interface _Publisher
|
|
9
|
+
def publish: (String event_name, Hash[Symbol, untyped] payload) -> void
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
@target: _Publisher
|
|
13
|
+
|
|
14
|
+
@event_name: String
|
|
15
|
+
|
|
16
|
+
# Publishes to `target` - a Sossego stream, or anything with `publish(event_name, payload)`
|
|
17
|
+
# : (_Publisher, ?event_name: String) -> void
|
|
18
|
+
def initialize: (_Publisher, ?event_name: String) -> void
|
|
19
|
+
|
|
20
|
+
# Publishes the batch as a JSON-ready Hash
|
|
21
|
+
# : (Batch, Log) -> void
|
|
22
|
+
def call: (Batch, Log) -> void
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Generated from lib/task_progress/ractor_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Sends each batch to a Ractor::Port, as a deep frozen copy - for a tree built inside a
|
|
5
|
+
# Ractor and mounted outside it: `mount << port.receive`. Ruby 4.0 and up only
|
|
6
|
+
class RactorOutput
|
|
7
|
+
@port: untyped
|
|
8
|
+
|
|
9
|
+
# Sends to a `Ractor::Port`. Raises NotImplementedError before Ruby 4.0
|
|
10
|
+
# : (untyped) -> void
|
|
11
|
+
def initialize: (untyped) -> void
|
|
12
|
+
|
|
13
|
+
# Sends a deep frozen copy of the batch to the port
|
|
14
|
+
# : (Batch, Log) -> void
|
|
15
|
+
def call: (Batch, Log) -> void
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated from lib/task_progress/rails_defaults.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# What TaskProgress.broadcast and the `task_progress_tree` helper use when Rails is there and
|
|
5
|
+
# nothing else is given: the record's dom_id, Turbo::StreamsChannel, Rails.cache, and tokens
|
|
6
|
+
# signed with a key the app's secret_key_base derives. Without Rails, nothing - and the caller
|
|
7
|
+
# says what to broadcast to. The record is streamed to as `[record, :task_progress]`
|
|
8
|
+
module RailsDefaults
|
|
9
|
+
# The id the tree's element has, for a record or a string
|
|
10
|
+
# : (untyped) -> String
|
|
11
|
+
def self.id: (untyped) -> String
|
|
12
|
+
|
|
13
|
+
# The store key the latest frame is kept under
|
|
14
|
+
# : (untyped) -> String
|
|
15
|
+
def self.key: (untyped) -> String
|
|
16
|
+
|
|
17
|
+
# The defaults for what is not given, or nothing outside Rails
|
|
18
|
+
# : (untyped) -> Hash[Symbol, untyped]
|
|
19
|
+
def self.for: (untyped) -> Hash[Symbol, untyped]
|
|
20
|
+
|
|
21
|
+
# Tokens for the cancel buttons, signed with a key derived from the app's secret
|
|
22
|
+
# : () -> Token
|
|
23
|
+
def self.tokens: () -> Token
|
|
24
|
+
|
|
25
|
+
# Where the cancel buttons post, or nil when the app has turned that off
|
|
26
|
+
# : () -> String?
|
|
27
|
+
def self.cancellations_path: () -> String?
|
|
28
|
+
|
|
29
|
+
# Whether this runs in a Rails app
|
|
30
|
+
# : () -> bool
|
|
31
|
+
def self.rails?: () -> bool
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Generated from lib/task_progress/recording_output.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Keeps every batch it gets, for a test to look through:
|
|
5
|
+
#
|
|
6
|
+
# recording = TaskProgress::RecordingOutput.new
|
|
7
|
+
# Importer.new(progress: TaskProgress.new("Import", outputs: [recording])).run
|
|
8
|
+
# assert_includes recording.said, "Skipped 3 rows"
|
|
9
|
+
# assert_equal 1, recording.events(:fail).length
|
|
10
|
+
#
|
|
11
|
+
# Safe to share between threads
|
|
12
|
+
class RecordingOutput
|
|
13
|
+
@batches: Array[Batch]
|
|
14
|
+
|
|
15
|
+
@lock: Thread::Mutex
|
|
16
|
+
|
|
17
|
+
# Nothing recorded yet
|
|
18
|
+
# : () -> void
|
|
19
|
+
def initialize: () -> void
|
|
20
|
+
|
|
21
|
+
# Records the batch
|
|
22
|
+
# : (Batch, Log) -> void
|
|
23
|
+
def call: (Batch, Log) -> void
|
|
24
|
+
|
|
25
|
+
# Every batch so far, in order
|
|
26
|
+
# : () -> Array[Batch]
|
|
27
|
+
def batches: () -> Array[Batch]
|
|
28
|
+
|
|
29
|
+
# Every event so far, in order - only those with the op `op`, if given
|
|
30
|
+
# : (?Symbol?) -> Array[Event]
|
|
31
|
+
def events: (?Symbol?) -> Array[Event]
|
|
32
|
+
|
|
33
|
+
# What the tree was told to `puts`, in order
|
|
34
|
+
# : () -> Array[String]
|
|
35
|
+
def said: () -> Array[String]
|
|
36
|
+
|
|
37
|
+
# The tree the batches make, as an outline - see TaskProgress#outline
|
|
38
|
+
# : () -> Hash[Symbol, untyped]?
|
|
39
|
+
def outline: () -> Hash[Symbol, untyped]?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Generated from lib/task_progress/redis_stream_store.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# A stream store in Redis: each tree a Redis stream of its own (XADD, XREAD, XTRIM), its values in plain keys. It
|
|
5
|
+
# takes a redis-client connection or pool (`RedisClient.config(url:).new_pool`), or a redis-rb
|
|
6
|
+
# `Redis` - neither is a dependency of this gem, and nothing here loads one.
|
|
7
|
+
#
|
|
8
|
+
# TaskProgress.stream_store = TaskProgress::RedisStreamStore.new(RedisClient.config(url: ENV["REDIS_URL"]).new_pool)
|
|
9
|
+
#
|
|
10
|
+
# Needs Redis 6.2 or later, for trimming by id. A reader that waits holds its connection for
|
|
11
|
+
# as long as it waits - a pool sized for the readers that follow at once keeps the rest moving.
|
|
12
|
+
# A redis-rb `Redis` is a single connection, so everything else on it waits too: give one that
|
|
13
|
+
# follows trees a client of its own.
|
|
14
|
+
#
|
|
15
|
+
# A write with a `guard:` - a key and the value it must hold - runs as a Lua script, so the
|
|
16
|
+
# check and the write are one step, see MemoryStreamStore.
|
|
17
|
+
#
|
|
18
|
+
# Every tree is under its own key already, so two users' trees never meet. A `namespace` puts
|
|
19
|
+
# all the keys under it as well - `tenant-42:task_progress:{...}` - for apps, or tenants, that
|
|
20
|
+
# share a Redis and must stay apart even so
|
|
21
|
+
class RedisStreamStore
|
|
22
|
+
# The guarded writes. KEYS[1] is the guard, ARGV[1] the value it must hold
|
|
23
|
+
GUARDED: Hash[Symbol, String]
|
|
24
|
+
|
|
25
|
+
@redis: untyped
|
|
26
|
+
|
|
27
|
+
@namespace: String?
|
|
28
|
+
|
|
29
|
+
# For a redis-client connection or pool, or a redis-rb client
|
|
30
|
+
# : (untyped, ?namespace: String?) -> void
|
|
31
|
+
def initialize: (untyped, ?namespace: String?) -> void
|
|
32
|
+
|
|
33
|
+
# Adds `data` at the end of the stream at `key`, and returns its id
|
|
34
|
+
# : (String, String) -> String
|
|
35
|
+
def append: (String, String) -> String
|
|
36
|
+
|
|
37
|
+
# The entries after the id `after` - all of them when it is nil - as `[id, data]` pairs. With
|
|
38
|
+
# a `timeout`, waits up to that many seconds for one to come when there are none yet
|
|
39
|
+
# : (String, ?after: String?, ?timeout: (Integer | Float)?) -> Array[[String, String]]
|
|
40
|
+
def read: (String, ?after: String?, ?timeout: (Integer | Float)?) -> Array[[ String, String ]]
|
|
41
|
+
|
|
42
|
+
# The value at `key`, or nil
|
|
43
|
+
# : (String) -> String?
|
|
44
|
+
def get: (String) -> String?
|
|
45
|
+
|
|
46
|
+
# Keeps `value` at `key`, for `expires_in` seconds if given
|
|
47
|
+
# : (String, String, ?expires_in: (Integer | Float)?, ?guard: [String, String]?) -> bool
|
|
48
|
+
def set: (String, String, ?expires_in: (Integer | Float)?, ?guard: [ String, String ]?) -> bool
|
|
49
|
+
|
|
50
|
+
# Forgets the streams and values at these keys
|
|
51
|
+
# : (*String, ?guard: [String, String]?) -> bool
|
|
52
|
+
def delete: (*String, ?guard: [ String, String ]?) -> bool
|
|
53
|
+
|
|
54
|
+
# Drops the entries before the id `before`
|
|
55
|
+
# : (String, before: String, ?guard: [String, String]?) -> bool
|
|
56
|
+
def trim: (String, before: String, ?guard: [ String, String ]?) -> bool
|
|
57
|
+
|
|
58
|
+
# Forgets what is at `key` - a stream or a value - `seconds` from now
|
|
59
|
+
# : (String, Integer | Float, ?guard: [String, String]?) -> bool
|
|
60
|
+
def expire: (String, Integer | Float, ?guard: [ String, String ]?) -> bool
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# : (Symbol, [String, String], Array[String], Array[String]) -> bool
|
|
65
|
+
def guarded: (Symbol, [ String, String ], Array[String], Array[String]) -> bool
|
|
66
|
+
|
|
67
|
+
# : (String) -> String
|
|
68
|
+
def ns: (String) -> String
|
|
69
|
+
|
|
70
|
+
# XREAD answers in one of three shapes - RESP3 a Hash, RESP2 nested Arrays, redis-rb a Hash
|
|
71
|
+
# of Hashes - and nil when nothing came
|
|
72
|
+
# : (untyped, String) -> Array[[String, String]]
|
|
73
|
+
def entries: (untyped, String) -> Array[[ String, String ]]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Generated from lib/task_progress/squasher.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
class TaskProgress
|
|
4
|
+
# Squashes runs of batches into one. It mirrors the tree they build, so for a run it can
|
|
5
|
+
# send the prunes it saw and then a put for every node the run touched - which lands any
|
|
6
|
+
# reader that had the tree up to the run's `prev` exactly where the run left it, times
|
|
7
|
+
# and all, without replaying the steps in between. What was said is not in the tree, so
|
|
8
|
+
# it goes along after the puts
|
|
9
|
+
class Squasher
|
|
10
|
+
@state: State
|
|
11
|
+
|
|
12
|
+
# Starts from an empty tree
|
|
13
|
+
# : () -> void
|
|
14
|
+
def initialize: () -> void
|
|
15
|
+
|
|
16
|
+
# One batch that lands a reader where the run of batches would have
|
|
17
|
+
# : (Array[Batch]) -> Batch
|
|
18
|
+
def squash: (Array[Batch]) -> Batch
|
|
19
|
+
end
|
|
20
|
+
end
|