zizq 0.1.0

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +94 -0
  4. data/bin/profile-worker +145 -0
  5. data/bin/zizq-worker +174 -0
  6. data/lib/active_job/queue_adapters/zizq_adapter.rb +109 -0
  7. data/lib/zizq/ack_processor.rb +132 -0
  8. data/lib/zizq/active_job_config.rb +122 -0
  9. data/lib/zizq/backoff.rb +50 -0
  10. data/lib/zizq/bulk_enqueue.rb +87 -0
  11. data/lib/zizq/client.rb +982 -0
  12. data/lib/zizq/configuration.rb +164 -0
  13. data/lib/zizq/enqueue_request.rb +178 -0
  14. data/lib/zizq/enqueue_with.rb +109 -0
  15. data/lib/zizq/error.rb +43 -0
  16. data/lib/zizq/job.rb +188 -0
  17. data/lib/zizq/job_config.rb +244 -0
  18. data/lib/zizq/lifecycle.rb +58 -0
  19. data/lib/zizq/middleware.rb +79 -0
  20. data/lib/zizq/query.rb +566 -0
  21. data/lib/zizq/resources/error_enumerator.rb +241 -0
  22. data/lib/zizq/resources/error_page.rb +19 -0
  23. data/lib/zizq/resources/error_record.rb +19 -0
  24. data/lib/zizq/resources/job.rb +124 -0
  25. data/lib/zizq/resources/job_page.rb +57 -0
  26. data/lib/zizq/resources/page.rb +77 -0
  27. data/lib/zizq/resources/resource.rb +45 -0
  28. data/lib/zizq/resources.rb +16 -0
  29. data/lib/zizq/version.rb +9 -0
  30. data/lib/zizq/worker.rb +467 -0
  31. data/lib/zizq.rb +269 -0
  32. data/sig/generated/zizq/ack_processor.rbs +73 -0
  33. data/sig/generated/zizq/active_job_config.rbs +74 -0
  34. data/sig/generated/zizq/backoff.rbs +34 -0
  35. data/sig/generated/zizq/bulk_enqueue.rbs +72 -0
  36. data/sig/generated/zizq/client.rbs +419 -0
  37. data/sig/generated/zizq/configuration.rbs +95 -0
  38. data/sig/generated/zizq/enqueue_request.rbs +94 -0
  39. data/sig/generated/zizq/enqueue_with.rbs +88 -0
  40. data/sig/generated/zizq/error.rbs +41 -0
  41. data/sig/generated/zizq/job.rbs +136 -0
  42. data/sig/generated/zizq/job_config.rbs +150 -0
  43. data/sig/generated/zizq/lifecycle.rbs +34 -0
  44. data/sig/generated/zizq/middleware.rbs +50 -0
  45. data/sig/generated/zizq/query.rbs +327 -0
  46. data/sig/generated/zizq/resources/error_enumerator.rbs +148 -0
  47. data/sig/generated/zizq/resources/error_page.rbs +13 -0
  48. data/sig/generated/zizq/resources/error_record.rbs +20 -0
  49. data/sig/generated/zizq/resources/job.rbs +89 -0
  50. data/sig/generated/zizq/resources/job_page.rbs +33 -0
  51. data/sig/generated/zizq/resources/page.rbs +47 -0
  52. data/sig/generated/zizq/resources/resource.rbs +26 -0
  53. data/sig/generated/zizq/version.rbs +5 -0
  54. data/sig/generated/zizq/worker.rbs +152 -0
  55. data/sig/generated/zizq.rbs +180 -0
  56. data/sig/zizq.rbs +111 -0
  57. metadata +134 -0
data/lib/zizq.rb ADDED
@@ -0,0 +1,269 @@
1
+ # Copyright (c) 2026 Chris Corbyn <chris@zizq.io>
2
+ # Licensed under the MIT License. See LICENSE file for details.
3
+
4
+ # rbs_inline: enabled
5
+ # frozen_string_literal: true
6
+
7
+ require_relative "zizq/version"
8
+ require_relative "zizq/error"
9
+ require_relative "zizq/configuration"
10
+
11
+ # Autoloaded when first accessed — avoids loading heavy deps at require time.
12
+ autoload :MessagePack, "msgpack"
13
+
14
+ module Zizq
15
+ autoload :AckProcessor, "zizq/ack_processor"
16
+ autoload :ActiveJobConfig, "zizq/active_job_config"
17
+ autoload :Backoff, "zizq/backoff"
18
+ autoload :BulkEnqueue, "zizq/bulk_enqueue"
19
+ autoload :Client, "zizq/client"
20
+ autoload :EnqueueRequest, "zizq/enqueue_request"
21
+ autoload :EnqueueWith, "zizq/enqueue_with"
22
+ autoload :Job, "zizq/job"
23
+ autoload :JobConfig, "zizq/job_config"
24
+ autoload :Middleware, "zizq/middleware"
25
+ autoload :Lifecycle, "zizq/lifecycle"
26
+ autoload :Query, "zizq/query"
27
+ autoload :Resources, "zizq/resources"
28
+ autoload :Worker, "zizq/worker"
29
+
30
+ # Sentinel indicating a field should not be included in the request.
31
+ # Used as the default for update parameters.
32
+ module UNCHANGED; end
33
+
34
+ # Sentinel indicating a field should be sent as null to reset to server default.
35
+ module RESET; end
36
+
37
+ @client_mutex = Mutex.new
38
+
39
+ class << self
40
+ # Returns the client configuration.
41
+ #
42
+ # The configuration can be updated by calling [`Zizq::configure`].
43
+ #
44
+ # This configuration is for the client only. Worker parameters are
45
+ # configured on a per-run basis for flexibility.
46
+ def configuration #: () -> Configuration
47
+ @configuration ||= Configuration.new
48
+ end
49
+
50
+ # Yields the global configuration ready for updates, which should be done
51
+ # during application initialization, before any jobs are enqueued or
52
+ # worked.
53
+ #
54
+ # Zizq.configure do |c|
55
+ # c.url = "http://localhost:7890"
56
+ # c.format = :msgpack
57
+ # c.dequeue_middleware.use(MyDequeueMiddleware.new)
58
+ # end
59
+ def configure #: () { (Configuration) -> void } -> void
60
+ yield configuration
61
+ ensure
62
+ @client = nil # shared client is potentially stale
63
+ end
64
+
65
+ # Returns a shared client instance built from the global configuration.
66
+ #
67
+ # The client is memoized so that persistent HTTP connections are reused
68
+ # across calls, reducing TCP connection overhead.
69
+ def client #: () -> Client
70
+ @client ||= begin
71
+ @client_mutex.synchronize do
72
+ break @client if @client
73
+
74
+ configuration.validate!
75
+ @client = Client.new(
76
+ url: configuration.url,
77
+ format: configuration.format,
78
+ ssl_context: configuration.ssl_context
79
+ )
80
+ end
81
+ end
82
+ end
83
+
84
+ # Resets all global state: configuration and shared client.
85
+ # Intended for use in tests.
86
+ def reset! #: () -> void
87
+ @client&.close
88
+ @client = nil
89
+ @configuration = nil
90
+ end
91
+
92
+ # Server version string.
93
+ def server_version #: () -> String
94
+ client.server_version
95
+ end
96
+
97
+ # List all distinct queue names on the server.
98
+ def queues #: () -> Array[String]
99
+ client.get_queues
100
+ end
101
+
102
+ # Start a query to retrieve or modify job data.
103
+ #
104
+ # @rbs id: (String | Array[String])?
105
+ # @rbs queue: (String | Array[String])?
106
+ # @rbs type: (String | Array[String])?
107
+ # @rbs status: (String | Array[String])?
108
+ # @rbs jq_filter: String?
109
+ # @rbs order: Zizq::sort_direction?
110
+ # @rbs limit: Integer?
111
+ # @rbs page_size: Integer?
112
+ # @rbs return: Zizq::Query
113
+ def query(...)
114
+ Query.new(...)
115
+ end
116
+
117
+ # Enqueue a job by class with positional and keyword arguments.
118
+ #
119
+ # By default all arguments are serialized as JSON, which means hashes with
120
+ # symbol keys will become hashes with string keys. The serialization
121
+ # behaviour can be changed by implementing `::zizq_serialize` and
122
+ # `::zizq_deserialize` as class methods on the job class.
123
+ #
124
+ # Default job options can be overridden at enqueue-time by providing a
125
+ # block which receives a mutable `Zizq::EnqueueRequest` instance.
126
+ #
127
+ # Zizq.enqueue(SendEmailJob, 42, template: "welcome")
128
+ # Zizq.enqueue(SendEmailJob, 42) { |o| o.queue = "priority" }
129
+ #
130
+ # Job classes may also override `::zizq_enqueue_options` to implement
131
+ # dynamically computed options, such as dynamic prioritisation. This class
132
+ # method accepts the same arguments as the `#perform` method and returns an
133
+ # instance of `Zizq::EnqueueRequest`. Any overrides may call `super` and
134
+ # modify the result.
135
+ #
136
+ # class SendEmailJob
137
+ # include Zizq::Job
138
+ #
139
+ # zizq_priority 1000
140
+ #
141
+ # def self.zizq_enqueue_options(user_id, template:)
142
+ # opts = super
143
+ # opts.priority /= 2 if template == "welcome"
144
+ # opts
145
+ # end
146
+ #
147
+ # def perform(user_id, template:)
148
+ # # ...
149
+ # end
150
+ # end
151
+ #
152
+ # @rbs job_class: Class & Zizq::job_class
153
+ # @rbs args: Array[untyped]
154
+ # @rbs kwargs: Hash[Symbol, untyped]
155
+ # @rbs &block: ?(EnqueueRequest) -> void
156
+ # @rbs return: Resources::Job
157
+ def enqueue(job_class, *args, **kwargs, &block)
158
+ req = build_enqueue_request(job_class, *args, **kwargs, &block)
159
+ req = configuration.enqueue_middleware.call(req)
160
+ client.enqueue(**req.to_enqueue_params)
161
+ end
162
+
163
+ # Enqueue a job by providing raw inputs to the Zizq server.
164
+ #
165
+ # This is for advanced use cases such as enqueueing jobs for consumption in
166
+ # other programming languages.
167
+ #
168
+ # Zizq.enqueue_raw(
169
+ # queue: "emails",
170
+ # type: "send_email",
171
+ # payload: {user_id: 42, template: "welcome"}
172
+ # )
173
+ #
174
+ # If using this method to enqueue a job that is intended for consumption in
175
+ # the Ruby client itself a custom dispatcher implementation is likely
176
+ # required:
177
+ #
178
+ # Zizq.configure do |c|
179
+ # c.dispatcher = MyDispatcher.new
180
+ # end
181
+ #
182
+ # @rbs queue: String
183
+ # @rbs type: String
184
+ # @rbs payload: untyped
185
+ # @rbs priority: Integer?
186
+ # @rbs ready_at: Zizq::to_f?
187
+ # @rbs retry_limit: Integer?
188
+ # @rbs backoff: Zizq::backoff?
189
+ # @rbs retention: Zizq::retention?
190
+ # @rbs unique_key: String?
191
+ # @rbs unique_while: Zizq::unique_scope?
192
+ # @rbs return: Resources::Job
193
+ def enqueue_raw(queue:, type:, payload:, **opts)
194
+ req = EnqueueRequest.new(queue:, type:, payload:, **opts)
195
+ req = configuration.enqueue_middleware.call(req)
196
+ client.enqueue(**req.to_enqueue_params)
197
+ end
198
+
199
+ # Enqueue multiple jobs atomically in a single bulk request.
200
+ #
201
+ # This can significantly imprive throughput when many jobs need to be
202
+ # enqueued collectively. There is no upper limit on the number of jobs in
203
+ # the request though generally it is probably wise to keep this to less
204
+ # than 1000 jobs unless you have strong atomicity requuirements for a
205
+ # larger number of jobs..
206
+ #
207
+ # Yields a builder object whose `#enqueue` method accepts the same
208
+ # arguments as `Zizq.enqueue`. All collected jobs are sent as a
209
+ # single `POST /jobs/bulk` request and an array of jobs is returned in the
210
+ # same order as the inputs.
211
+ #
212
+ # Zizq.enqueue_bulk do |b|
213
+ # b.enqueue(ProcessPaymentJob, 7)
214
+ # b.enqueue(SendEmailJob, 42, template: "welcome")
215
+ # b.enqueue(SendEmailJob, 42) { |o| o.queue = "priority" }
216
+ # end
217
+ #
218
+ # Build a scoped enqueue helper that applies the given option overrides
219
+ # to every enqueue routed through it. Equivalent to using the block
220
+ # form of `Zizq.enqueue`, but composable and reusable.
221
+ #
222
+ # Zizq.enqueue_with(ready_at: Time.now + 3600).enqueue(MyJob, 42)
223
+ # Zizq.enqueue_with(priority: 0).enqueue_bulk { |b| ... }
224
+ #
225
+ # See `Zizq::EnqueueWith` for details.
226
+ #
227
+ # @rbs overrides: Hash[Symbol, untyped]
228
+ # @rbs return: EnqueueWith
229
+ def enqueue_with(**overrides)
230
+ EnqueueWith.new(self, overrides)
231
+ end
232
+
233
+ # @rbs &block: (BulkEnqueue) -> void
234
+ # @rbs return: Array[Resources::Job]
235
+ def enqueue_bulk(&block)
236
+ builder = BulkEnqueue.new
237
+ yield builder
238
+ return [] if builder.requests.empty?
239
+
240
+ jobs = builder.requests.map do |req|
241
+ configuration.enqueue_middleware.call(req).to_enqueue_params
242
+ end
243
+
244
+ client.enqueue_bulk(jobs:)
245
+ end
246
+
247
+ # @api private
248
+ # Build an EnqueueRequest for a single job class enqueue.
249
+ #
250
+ # @rbs job_class: Class & Zizq::job_class
251
+ # @rbs args: Array[untyped]
252
+ # @rbs kwargs: Hash[Symbol, untyped]
253
+ # @rbs &block: ?(EnqueueRequest) -> void
254
+ # @rbs return: EnqueueRequest
255
+ def build_enqueue_request(job_class, *args, **kwargs, &block)
256
+ unless job_class.is_a?(Class) && job_class < Zizq::Job
257
+ raise ArgumentError, "#{job_class.inspect} must include Zizq::Job"
258
+ end
259
+
260
+ zizq_job_class = job_class #: Zizq::job_class
261
+ req = zizq_job_class.zizq_enqueue_request(*args, **kwargs)
262
+ yield req if block_given?
263
+ req
264
+ end
265
+ end
266
+ end
267
+
268
+ # Make sure everything is cleaned up before we exit.
269
+ Kernel.at_exit { Zizq.reset! }
@@ -0,0 +1,73 @@
1
+ # Generated from lib/zizq/ack_processor.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Dedicated background thread that processes ack/nack HTTP requests on
5
+ # behalf of worker threads, decoupling job processing from network I/O.
6
+ #
7
+ # Workers push Ack/Nack items to a thread-safe queue. The processor runs
8
+ # an async event loop that spawns an independent fiber per ack/nack
9
+ # request, enabling true concurrent I/O over a single HTTP/2 connection.
10
+ # Each fiber handles its own retries with exponential backoff.
11
+ class AckProcessor
12
+ # Immutable value object representing a successful job completion.
13
+ class Ack < Data
14
+ attr_reader job_id(): untyped
15
+
16
+ def self.new: (untyped job_id) -> instance
17
+ | (job_id: untyped) -> instance
18
+
19
+ def self.members: () -> [ :job_id ]
20
+
21
+ def members: () -> [ :job_id ]
22
+ end
23
+
24
+ # Immutable value object representing a job failure.
25
+ class Nack < Data
26
+ attr_reader job_id(): untyped
27
+
28
+ attr_reader message(): untyped
29
+
30
+ attr_reader error_type(): untyped
31
+
32
+ attr_reader backtrace(): untyped
33
+
34
+ def self.new: (untyped job_id, untyped message, untyped error_type, untyped backtrace) -> instance
35
+ | (job_id: untyped, message: untyped, error_type: untyped, backtrace: untyped) -> instance
36
+
37
+ def self.members: () -> [ :job_id, :message, :error_type, :backtrace ]
38
+
39
+ def members: () -> [ :job_id, :message, :error_type, :backtrace ]
40
+ end
41
+
42
+ # @rbs client: Client
43
+ # @rbs capacity: Integer
44
+ # @rbs logger: Logger
45
+ # @rbs backoff: Backoff
46
+ # @rbs return: void
47
+ def initialize: (client: Client, capacity: Integer, logger: Logger, backoff: Backoff) -> void
48
+
49
+ # Push an Ack or Nack to the processing queue.
50
+ # Blocks if the queue is at capacity (backpressure).
51
+ #
52
+ # @rbs item: Ack | Nack
53
+ # @rbs return: void
54
+ def push: (Ack | Nack item) -> void
55
+
56
+ # Start the background processor thread.
57
+ def start: () -> untyped
58
+
59
+ # Close the queue and wait for the processor to drain. Waits indefinitely —
60
+ # callers who want a deadline should wrap the call in `Timeout::timeout`.
61
+ #
62
+ # @rbs return: void
63
+ def stop: () -> void
64
+
65
+ private
66
+
67
+ def run: () -> untyped
68
+
69
+ def process_ack_batch: (untyped acks) -> untyped
70
+
71
+ def process_nack: (untyped nack) -> untyped
72
+ end
73
+ end
@@ -0,0 +1,74 @@
1
+ # Generated from lib/zizq/active_job_config.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Zizq configuration DSL for ActiveJob classes.
5
+ #
6
+ # Extend this module in an ActiveJob subclass to gain access to Zizq
7
+ # features like unique jobs, backoff, and retention:
8
+ #
9
+ # class SendEmailJob < ApplicationJob
10
+ # extend Zizq::ActiveJobConfig
11
+ #
12
+ # zizq_unique true, scope: :active
13
+ # zizq_backoff exponent: 4.0, base: 15, jitter: 30
14
+ #
15
+ # def perform(user_id, template:)
16
+ # # ...
17
+ # end
18
+ # end
19
+ #
20
+ # Serialization uses ActiveJob's own format so that GlobalID, Time, and
21
+ # other ActiveJob-supported types are handled correctly. The Zizq worker
22
+ # must use the ActiveJob dispatcher:
23
+ #
24
+ # Zizq.configure do |c|
25
+ # c.dispatcher = ActiveJob::QueueAdapters::ZizqAdapter::Dispatcher
26
+ # end
27
+ module ActiveJobConfig
28
+ include JobConfig
29
+
30
+ # ActiveJob::Base.new — invisible to steep without this.
31
+ def new: (*untyped, **untyped) -> untyped
32
+
33
+ # Serialize arguments using ActiveJob's serialization format.
34
+ #
35
+ # Creates a temporary ActiveJob instance to produce the canonical
36
+ # serialized form, including `_aj_ruby2_keywords` markers for kwargs.
37
+ # This ensures unique key generation uses the same format as the
38
+ # enqueued payload.
39
+ #
40
+ # This is needed so that unique job keys can be correctly generated.
41
+ def zizq_serialize: (*untyped args, **untyped kwargs) -> untyped
42
+
43
+ # Deserialization is handled by ActiveJob::Base.execute on the worker
44
+ # side. This method is not used in the ActiveJob dispatch path.
45
+ def zizq_deserialize: (untyped _payload) -> untyped
46
+
47
+ # Generate a jq expression that exactly matches payloads with the given
48
+ # arguments.
49
+ #
50
+ # This is used for filtering in Zizq::Query.
51
+ #
52
+ # Generates an expression of the form:
53
+ #
54
+ # .arguments == ["a","b",{"example":true,"_aj_ruby2_keywords":["example"]}]
55
+ def zizq_payload_filter: (*untyped args, **untyped kwargs) -> untyped
56
+
57
+ # Generate a jq expression that matches jobs whose positional args
58
+ # start with the given values and whose kwargs contain the given
59
+ # key/value pairs.
60
+ #
61
+ # This is used for filtering in Zizq::Query.
62
+ #
63
+ # Generates expressions of the form:
64
+ #
65
+ # (.arguments[0:2] == ["a","b"])
66
+ #
67
+ # or
68
+ #
69
+ # (.arguments[0:2] == ["a","b"]) and
70
+ # (.arguments[-1] | has("_aj_ruby2_keywords")) and
71
+ # (.arguments[-1] | contains({"example":true}))
72
+ def zizq_payload_subset_filter: (*untyped args, **untyped kwargs) -> untyped
73
+ end
74
+ end
@@ -0,0 +1,34 @@
1
+ # Generated from lib/zizq/backoff.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Encapsulates exponential backoff state for retry loops.
5
+ #
6
+ # Each call to `wait` sleeps for the current duration and then advances
7
+ # to the next interval. Call `reset` to return to the initial wait time
8
+ # after a successful operation.
9
+ class Backoff
10
+ attr_reader min_wait: Float
11
+
12
+ attr_reader max_wait: Float
13
+
14
+ attr_reader multiplier: Float
15
+
16
+ # @rbs min_wait: (Float | Integer)
17
+ # @rbs max_wait: (Float | Integer)
18
+ # @rbs multiplier: (Float | Integer)
19
+ # @rbs return: void
20
+ def initialize: (min_wait: Float | Integer, max_wait: Float | Integer, multiplier: Float | Integer) -> void
21
+
22
+ # Returns the current backoff duration without advancing.
23
+ def duration: () -> untyped
24
+
25
+ # Sleeps for the current backoff duration, then advances to the next.
26
+ def wait: () -> untyped
27
+
28
+ # Resets the backoff to the initial min_wait.
29
+ def reset: () -> untyped
30
+
31
+ # Returns a new Backoff with the same configuration but reset state.
32
+ def fresh: () -> untyped
33
+ end
34
+ end
@@ -0,0 +1,72 @@
1
+ # Generated from lib/zizq/bulk_enqueue.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Builder for collecting multiple job params to be sent as a single bulk
5
+ # request via `Zizq.enqueue_bulk`.
6
+ #
7
+ # Zizq.enqueue_bulk do |b|
8
+ # b.enqueue(MyApp::FooJob, 42)
9
+ # b.enqueue(MyApp::OtherJob, 42, x: 7)
10
+ # end
11
+ class BulkEnqueue
12
+ attr_reader requests: Array[EnqueueRequest]
13
+
14
+ def initialize: () -> untyped
15
+
16
+ # Collect a job class enqueue. Accepts the same arguments as
17
+ # `Zizq.enqueue`.
18
+ #
19
+ # @rbs job_class: Class & Zizq::job_class
20
+ # @rbs args: Array[untyped]
21
+ # @rbs kwargs: Hash[Symbol, untyped]
22
+ # @rbs &block: ?(EnqueueRequest) -> void
23
+ # @rbs return: void
24
+ def enqueue: (Class & Zizq::job_class job_class, *untyped args, **untyped kwargs) ?{ (EnqueueRequest) -> void } -> void
25
+
26
+ # Collect a raw enqueue. Accepts the same arguments as
27
+ # `Zizq.enqueue_raw`.
28
+ #
29
+ # @rbs queue: String
30
+ # @rbs type: String
31
+ # @rbs payload: untyped
32
+ # @rbs priority: Integer?
33
+ # @rbs ready_at: Zizq::to_f?
34
+ # @rbs retry_limit: Integer?
35
+ # @rbs backoff: Zizq::backoff?
36
+ # @rbs retention: Zizq::retention?
37
+ # @rbs unique_key: String?
38
+ # @rbs unique_while: Zizq::unique_scope?
39
+ # @rbs return: void
40
+ def enqueue_raw: (queue: String, type: String, payload: untyped, **untyped opts) -> void
41
+
42
+ # Build a scoped enqueue helper that applies the given overrides to a
43
+ # single enqueue inside this bulk block. Sugar for the block form:
44
+ #
45
+ # b.enqueue_with(ready_at: Time.now + 3600).enqueue(OtherJob, 42)
46
+ #
47
+ # is equivalent to:
48
+ #
49
+ # b.enqueue(OtherJob, 42) { |req| req.ready_at = Time.now + 3600 }
50
+ #
51
+ # @rbs overrides: Hash[Symbol, untyped]
52
+ # @rbs return: EnqueueWith
53
+ def enqueue_with: (**untyped overrides) -> EnqueueWith
54
+
55
+ # Nested bulk is a no-op — we're already inside a bulk block, so we
56
+ # just yield this same builder. This exists to satisfy the
57
+ # `_EnqueueTarget` interface, which lets `EnqueueWith#enqueue_bulk`
58
+ # work uniformly against both the top-level `Zizq` module and a
59
+ # `BulkEnqueue` instance without branching on target type.
60
+ #
61
+ # Zizq.enqueue_bulk do |b|
62
+ # b.enqueue_with(priority: 0).enqueue_bulk do |b2|
63
+ # b2.enqueue(MyJob, 1)
64
+ # b2.enqueue(MyJob, 2)
65
+ # end
66
+ # end
67
+ #
68
+ # @rbs &block: (BulkEnqueue) -> void
69
+ # @rbs return: self
70
+ def enqueue_bulk: () { (BulkEnqueue) -> void } -> self
71
+ end
72
+ end