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,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