zizq 0.3.5 → 0.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d7eb0e9668c055e6f5b6b8ec6aacb5c09f8df5eedcea038acdb14d164a58acf
4
- data.tar.gz: d30b95be8bab2eae5100b89fa7957278a20c301538e5630156bf9892739864a3
3
+ metadata.gz: 3252547a350b856e8122d15a77275769f62a4495efe36b36e252eba2246b51cf
4
+ data.tar.gz: 570e3ef065cbe42cfc038d6c1dd6669940d62f051d53965c48bea548f329e941
5
5
  SHA512:
6
- metadata.gz: dbe8d111b4083de907b4522ae4f0c110554a0ab5821a544d808c363868602178c15f983140436da6525b96ab30be159ac52309adfb05cb271739a7aee73174cc
7
- data.tar.gz: 39606af98c372c50147922ba7640dcd9e58d449c63927008abbcc03ac2b926e68d4856764152f51f16fb26554241e801df1fb862542e405a1087bd2213d4eaab
6
+ metadata.gz: 545bc4510f7df1acc51817c48ec2fae04c3a96df3589c2e3f12017d8bebb311a69cd7d18b14941ed3206829d08dd1cf28dd1d0461d44c7d2a265eead57f70710
7
+ data.tar.gz: 00b8916adf6921aded73d0f16cde1c2c52c98f338b3a9e4ad79a4131f06aef42b4001623c2e43d53a7c9d67e289c62f97b6abfbee6b0bb240ed89726e273ae7e
data/README.md CHANGED
@@ -12,7 +12,7 @@ API.
12
12
  ## Features
13
13
 
14
14
  * Multi-thread and/or multi-fiber concurrent worker (via [`async`](https://github.com/socketry/async))
15
- * `Zizq::Job` based job classes, Active Job support, or completely custom
15
+ * `Zizq::Job` based job classes, Active Job support, or low-level/custom
16
16
  * Enqueue and process jobs from one language to another
17
17
  * Arbitrary named queues
18
18
  * Granular job priorities
@@ -22,6 +22,7 @@ API.
22
22
  * Recurring jobs (cron)
23
23
  * Job introspection and management APIs, with support for `jq` query filters
24
24
  * Unique jobs
25
+ * Testing helpers
25
26
 
26
27
  ## Installation
27
28
 
@@ -32,13 +33,13 @@ API.
32
33
  Add it to your application's `Gemfile`:
33
34
 
34
35
  ```ruby
35
- gem 'zizq', '~> 0.3.5'
36
+ gem 'zizq', '~> 0.3.7'
36
37
  ```
37
38
 
38
39
  Or install it manually:
39
40
 
40
41
  ```shell
41
- $ gem install zizq -v 0.3.5
42
+ $ gem install zizq -v 0.3.7
42
43
  ```
43
44
 
44
45
  Ruby **3.2.8 or newer** is required. Client and server share version
@@ -62,7 +63,7 @@ Zizq.configure do |c|
62
63
  # Optional worker defaults — applied to every Zizq::Worker
63
64
  # instance and to runs of the `zizq-worker` executable. Explicit
64
65
  # kwargs or CLI flags override these.
65
- c.worker.queues = ['emails', 'payments']
66
+ c.worker.queues = ['emails', 'payments']
66
67
  c.worker.fiber_count = 25
67
68
  end
68
69
  ```
@@ -148,10 +149,42 @@ Zizq.enqueue_bulk do |b|
148
149
  end
149
150
  ```
150
151
 
151
- > [!NOTE]
152
- > Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw` —
153
- > designed for cross-language workflows where, for example, a Ruby app
154
- > enqueues jobs consumed by a Go service.
152
+ Jobs can also be enqueued without `Zizq::Job` via `Zizq.enqueue_raw` —
153
+ designed for lower-level code style, and for cross-language workflows where,
154
+ for example, a Ruby app enqueues jobs consumed by a Go service.
155
+
156
+ ```ruby
157
+ Zizq.enqueue_raw(
158
+ type: "send_email",
159
+ queue: "comms",
160
+ payload: { user_id: 42, template: "welcome" }
161
+ )
162
+ ```
163
+
164
+ ### Cross-language and low-level dispatch
165
+
166
+ When a Ruby app needs to *process* jobs enqueued by another language
167
+ (or by `Zizq.enqueue_raw`), `Zizq::Router` maps `type` strings to
168
+ handler blocks operating on plain JSON payloads:
169
+
170
+ ```ruby
171
+ Zizq.configure do |c|
172
+ c.dispatcher = Zizq::Router.new do
173
+ route('send_email') do |payload|
174
+ Mailer.deliver(payload['user_id'], payload['template'])
175
+ end
176
+
177
+ # Apps that mix the two styles can fall back to Zizq::Job
178
+ # for anything not handled by an explicit route.
179
+ fallback { |job| Zizq::Job.call(job) }
180
+ end
181
+ end
182
+ ```
183
+
184
+ See [Custom Dispatchers](https://zizq.io/docs/clients/ruby/dispatchers.html)
185
+ for full details. Dispatchers in Zizq are just objects that implement `#call`
186
+ with a single `Zizq::Resources::Job` argument, and `Zizq::Router` is just a
187
+ dispatcher itself.
155
188
 
156
189
  ### Running a worker
157
190
 
@@ -0,0 +1,100 @@
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
+ module Zizq
8
+ # Dispatch jobs by `type` string, mapping each to a handler block.
9
+ #
10
+ # Designed for cross-language workflows: payloads are plain JSON
11
+ # values (Hashes / Arrays / strings / numbers), `type` is a String
12
+ # the producer agrees on with the consumer, and routes are
13
+ # registered explicitly — no `Zizq::Job` mixin involved.
14
+ #
15
+ # Zizq.configure do |c|
16
+ # c.dispatcher = Zizq::Router.new do
17
+ # route("send_email") do |payload|
18
+ # Mailer.deliver(payload["user_id"], payload["template"])
19
+ # end
20
+ #
21
+ # route("expire_tokens") do
22
+ # TokenSweeper.run
23
+ # end
24
+ #
25
+ # route("generate_report") do |payload, job|
26
+ # Reports.generate(payload["id"], attempts: job.attempts)
27
+ # end
28
+ #
29
+ # # `def` inside the block defines singleton methods on the
30
+ # # router. Route blocks captured *inside* the constructor
31
+ # # have lexical `self == router`, so they can call these
32
+ # # helpers; routes added outside (`router.route("…") { … }`)
33
+ # # keep their own lexical `self` and would need to go through
34
+ # # the router explicitly (`router.logger`).
35
+ # def logger
36
+ # Zizq.configuration.logger
37
+ # end
38
+ #
39
+ # # Anything else falls back. A common pattern is delegating
40
+ # # to `Zizq::Job` for the apps that mix the two styles.
41
+ # fallback { |job| Zizq::Job.call(job) }
42
+ # end
43
+ # end
44
+ #
45
+ # Routes can also be registered outside the constructor block:
46
+ #
47
+ # router = Zizq::Router.new
48
+ # router.route("send_email") { |payload| ... }
49
+ #
50
+ # Handlers are called as `handler.call(payload, job)`. Block-arity
51
+ # rules let `{ |payload| }` or `{ }` ignore either argument.
52
+ # Strict-arity lambdas need to declare both.
53
+ class Router
54
+ # Raised when a job arrives with a type that has no registered
55
+ # route and no fallback. Caught by Zizq's normal worker error
56
+ # path, which nacks the job for retry (or dead-letters it once
57
+ # the retry limit is hit).
58
+ class UnknownJobType < Zizq::Error; end
59
+
60
+ # @rbs &block: ?(self) [self: Router] -> void
61
+ def initialize(&block)
62
+ @routes = {} #: Hash[String, ^(untyped, Resources::Job) -> void]
63
+ @fallback = nil #: (^(Resources::Job) -> void)?
64
+ instance_eval(&block) if block
65
+ end
66
+
67
+ # Register `handler` for jobs whose `type` matches.
68
+ #
69
+ # @rbs type: String | Symbol
70
+ # @rbs &handler: (untyped, Resources::Job) -> void
71
+ def route(type, &handler)
72
+ @routes[type.to_s] = handler
73
+ end
74
+
75
+ # Register a fallback handler invoked when no route matches.
76
+ # Receives the full `Resources::Job` (not split into payload /
77
+ # job pair), since the canonical use is delegation to another
78
+ # dispatcher:
79
+ #
80
+ # fallback { |job| Zizq::Job.call(job) }
81
+ #
82
+ # @rbs &handler: (Resources::Job) -> void
83
+ def fallback(&handler)
84
+ @fallback = handler
85
+ end
86
+
87
+ # Dispatch a single job. Looks up the handler by `job.type`,
88
+ # falls back to the registered fallback if any, otherwise
89
+ # raises `UnknownJobType`.
90
+ def call(job) #: (Resources::Job) -> void
91
+ handler = @routes[job.type]
92
+
93
+ return handler.call(job.payload, job) if handler
94
+ return @fallback.call(job) if @fallback
95
+
96
+ raise UnknownJobType,
97
+ "no handler registered for job type #{job.type.inspect}"
98
+ end
99
+ end
100
+ end
@@ -4,6 +4,8 @@
4
4
  # rbs_inline: enabled
5
5
  # frozen_string_literal: true
6
6
 
7
+ require "json"
8
+
7
9
  module Zizq
8
10
  module Test
9
11
  # A `Zizq::Client` stand-in for use in test suites.
@@ -143,7 +145,7 @@ module Zizq
143
145
  req = EnqueueRequest.new(
144
146
  queue:,
145
147
  type:,
146
- payload:,
148
+ payload: self.class.normalize_payload(payload),
147
149
  priority:,
148
150
  ready_at:,
149
151
  retry_limit:,
@@ -162,7 +164,7 @@ module Zizq
162
164
  req = EnqueueRequest.new(
163
165
  queue: params[:queue],
164
166
  type: params[:type],
165
- payload: params[:payload],
167
+ payload: self.class.normalize_payload(params[:payload]),
166
168
  priority: params[:priority],
167
169
  ready_at: params[:ready_at],
168
170
  retry_limit: params[:retry_limit],
@@ -236,6 +238,22 @@ module Zizq
236
238
  "#{ID_PREFIX}#{counter.to_s.rjust(ID_LENGTH - ID_PREFIX.length, '0')}"
237
239
  end
238
240
 
241
+ public
242
+
243
+ # Round-trip the payload through JSON so the in-memory
244
+ # representation matches what a consumer would receive over the
245
+ # wire: symbol keys / Symbol values become strings, nested
246
+ # hashes and arrays are normalized recursively, and non-JSON-
247
+ # safe values (BigDecimal, custom objects) raise here rather
248
+ # than surviving in test mode only to break in production.
249
+ #
250
+ # Also used by `Zizq::Test.enqueued_raw?` / `enqueued_raw_count`
251
+ # to normalize the query side so symbol-keyed assertion payloads
252
+ # still match the (now string-keyed) buffer.
253
+ def self.normalize_payload(payload) #: (untyped) -> untyped
254
+ JSON.parse(JSON.generate(payload))
255
+ end
256
+
239
257
  # Returns entries matching every named filter AND the predicate.
240
258
  # All filter kwargs are optional; unset means "don't filter on
241
259
  # this axis." Callers must hold `@mutex` — public accessors do
data/lib/zizq/test.rb CHANGED
@@ -168,7 +168,13 @@ module Zizq
168
168
  filters = {}
169
169
  filters[:only_queues] = queue if queue
170
170
  filters[:only_types] = type if type
171
- filters[:filter] = ->(job) { job.payload == payload } unless payload.nil?
171
+ unless payload.nil?
172
+ # Normalize the assertion-side payload the same way enqueue
173
+ # normalizes the buffered one, so symbol-keyed test payloads
174
+ # still match the (string-keyed) wire-format buffer.
175
+ normalized = Client.normalize_payload(payload)
176
+ filters[:filter] = ->(job) { job.payload == normalized }
177
+ end
172
178
  client.enqueued_jobs(**filters).size
173
179
  end
174
180
 
data/lib/zizq/version.rb CHANGED
@@ -5,5 +5,5 @@
5
5
  # frozen_string_literal: true
6
6
 
7
7
  module Zizq
8
- VERSION = "0.3.5" #: String
8
+ VERSION = "0.3.7" #: String
9
9
  end
data/lib/zizq.rb CHANGED
@@ -29,6 +29,7 @@ module Zizq
29
29
  autoload :Lifecycle, "zizq/lifecycle"
30
30
  autoload :Query, "zizq/query"
31
31
  autoload :Resources, "zizq/resources"
32
+ autoload :Router, "zizq/router"
32
33
  autoload :Test, "zizq/test"
33
34
  autoload :TlsConfiguration, "zizq/tls_configuration"
34
35
  autoload :Worker, "zizq/worker"
@@ -0,0 +1,81 @@
1
+ # Generated from lib/zizq/router.rb with RBS::Inline
2
+
3
+ module Zizq
4
+ # Dispatch jobs by `type` string, mapping each to a handler block.
5
+ #
6
+ # Designed for cross-language workflows: payloads are plain JSON
7
+ # values (Hashes / Arrays / strings / numbers), `type` is a String
8
+ # the producer agrees on with the consumer, and routes are
9
+ # registered explicitly — no `Zizq::Job` mixin involved.
10
+ #
11
+ # Zizq.configure do |c|
12
+ # c.dispatcher = Zizq::Router.new do
13
+ # route("send_email") do |payload|
14
+ # Mailer.deliver(payload["user_id"], payload["template"])
15
+ # end
16
+ #
17
+ # route("expire_tokens") do
18
+ # TokenSweeper.run
19
+ # end
20
+ #
21
+ # route("generate_report") do |payload, job|
22
+ # Reports.generate(payload["id"], attempts: job.attempts)
23
+ # end
24
+ #
25
+ # # `def` inside the block defines singleton methods on the
26
+ # # router. Route blocks captured *inside* the constructor
27
+ # # have lexical `self == router`, so they can call these
28
+ # # helpers; routes added outside (`router.route("…") { … }`)
29
+ # # keep their own lexical `self` and would need to go through
30
+ # # the router explicitly (`router.logger`).
31
+ # def logger
32
+ # Zizq.configuration.logger
33
+ # end
34
+ #
35
+ # # Anything else falls back. A common pattern is delegating
36
+ # # to `Zizq::Job` for the apps that mix the two styles.
37
+ # fallback { |job| Zizq::Job.call(job) }
38
+ # end
39
+ # end
40
+ #
41
+ # Routes can also be registered outside the constructor block:
42
+ #
43
+ # router = Zizq::Router.new
44
+ # router.route("send_email") { |payload| ... }
45
+ #
46
+ # Handlers are called as `handler.call(payload, job)`. Block-arity
47
+ # rules let `{ |payload| }` or `{ }` ignore either argument.
48
+ # Strict-arity lambdas need to declare both.
49
+ class Router
50
+ # Raised when a job arrives with a type that has no registered
51
+ # route and no fallback. Caught by Zizq's normal worker error
52
+ # path, which nacks the job for retry (or dead-letters it once
53
+ # the retry limit is hit).
54
+ class UnknownJobType < Zizq::Error
55
+ end
56
+
57
+ # @rbs &block: ?(self) [self: Router] -> void
58
+ def initialize: () ?{ (self) [self: Router] -> void } -> untyped
59
+
60
+ # Register `handler` for jobs whose `type` matches.
61
+ #
62
+ # @rbs type: String | Symbol
63
+ # @rbs &handler: (untyped, Resources::Job) -> void
64
+ def route: (String | Symbol type) { (untyped, Resources::Job) -> void } -> untyped
65
+
66
+ # Register a fallback handler invoked when no route matches.
67
+ # Receives the full `Resources::Job` (not split into payload /
68
+ # job pair), since the canonical use is delegation to another
69
+ # dispatcher:
70
+ #
71
+ # fallback { |job| Zizq::Job.call(job) }
72
+ #
73
+ # @rbs &handler: (Resources::Job) -> void
74
+ def fallback: () { (Resources::Job) -> void } -> untyped
75
+
76
+ # Dispatch a single job. Looks up the handler by `job.type`,
77
+ # falls back to the registered fallback if any, otherwise
78
+ # raises `UnknownJobType`.
79
+ def call: (untyped job) -> untyped
80
+ end
81
+ end
@@ -120,6 +120,20 @@ module Zizq
120
120
 
121
121
  def synthetic_id: (untyped counter) -> untyped
122
122
 
123
+ public
124
+
125
+ # Round-trip the payload through JSON so the in-memory
126
+ # representation matches what a consumer would receive over the
127
+ # wire: symbol keys / Symbol values become strings, nested
128
+ # hashes and arrays are normalized recursively, and non-JSON-
129
+ # safe values (BigDecimal, custom objects) raise here rather
130
+ # than surviving in test mode only to break in production.
131
+ #
132
+ # Also used by `Zizq::Test.enqueued_raw?` / `enqueued_raw_count`
133
+ # to normalize the query side so symbol-keyed assertion payloads
134
+ # still match the (now string-keyed) buffer.
135
+ def self.normalize_payload: (untyped payload) -> untyped
136
+
123
137
  # Returns entries matching every named filter AND the predicate.
124
138
  # All filter kwargs are optional; unset means "don't filter on
125
139
  # this axis." Callers must hold `@mutex` — public accessors do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zizq
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Corbyn <chris@zizq.io>
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-27 00:00:00.000000000 Z
11
+ date: 2026-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async-http
@@ -100,6 +100,7 @@ files:
100
100
  - lib/zizq/resources/job_template.rb
101
101
  - lib/zizq/resources/page.rb
102
102
  - lib/zizq/resources/resource.rb
103
+ - lib/zizq/router.rb
103
104
  - lib/zizq/test.rb
104
105
  - lib/zizq/test/client.rb
105
106
  - lib/zizq/tls_configuration.rb
@@ -135,6 +136,7 @@ files:
135
136
  - sig/generated/zizq/resources/job_template.rbs
136
137
  - sig/generated/zizq/resources/page.rbs
137
138
  - sig/generated/zizq/resources/resource.rbs
139
+ - sig/generated/zizq/router.rbs
138
140
  - sig/generated/zizq/test.rbs
139
141
  - sig/generated/zizq/test/client.rbs
140
142
  - sig/generated/zizq/tls_configuration.rbs