nats_pubsub 1.0.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.
- checksums.yaml +7 -0
- data/exe/nats_pubsub +44 -0
- data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
- data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
- data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
- data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
- data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
- data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
- data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
- data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
- data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
- data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
- data/lib/nats_pubsub/active_record/publishable.rb +192 -0
- data/lib/nats_pubsub/cli.rb +105 -0
- data/lib/nats_pubsub/core/base_repository.rb +73 -0
- data/lib/nats_pubsub/core/config.rb +152 -0
- data/lib/nats_pubsub/core/config_presets.rb +139 -0
- data/lib/nats_pubsub/core/connection.rb +103 -0
- data/lib/nats_pubsub/core/constants.rb +190 -0
- data/lib/nats_pubsub/core/duration.rb +113 -0
- data/lib/nats_pubsub/core/error_action.rb +288 -0
- data/lib/nats_pubsub/core/event.rb +275 -0
- data/lib/nats_pubsub/core/health_check.rb +470 -0
- data/lib/nats_pubsub/core/logging.rb +72 -0
- data/lib/nats_pubsub/core/message_context.rb +193 -0
- data/lib/nats_pubsub/core/presets.rb +222 -0
- data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
- data/lib/nats_pubsub/core/structured_logger.rb +141 -0
- data/lib/nats_pubsub/core/subject.rb +185 -0
- data/lib/nats_pubsub/instrumentation.rb +327 -0
- data/lib/nats_pubsub/middleware/active_record.rb +18 -0
- data/lib/nats_pubsub/middleware/chain.rb +92 -0
- data/lib/nats_pubsub/middleware/logging.rb +48 -0
- data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
- data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
- data/lib/nats_pubsub/models/event_model.rb +73 -0
- data/lib/nats_pubsub/models/inbox_event.rb +109 -0
- data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
- data/lib/nats_pubsub/models/model_utils.rb +57 -0
- data/lib/nats_pubsub/models/outbox_event.rb +113 -0
- data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
- data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
- data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
- data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
- data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
- data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
- data/lib/nats_pubsub/publisher/publisher.rb +156 -0
- data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
- data/lib/nats_pubsub/railtie.rb +52 -0
- data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
- data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
- data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
- data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
- data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
- data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
- data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
- data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
- data/lib/nats_pubsub/subscribers/pool.rb +166 -0
- data/lib/nats_pubsub/subscribers/registry.rb +114 -0
- data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
- data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
- data/lib/nats_pubsub/subscribers/worker.rb +152 -0
- data/lib/nats_pubsub/tasks/install.rake +10 -0
- data/lib/nats_pubsub/testing/helpers.rb +199 -0
- data/lib/nats_pubsub/testing/matchers.rb +208 -0
- data/lib/nats_pubsub/testing/test_harness.rb +250 -0
- data/lib/nats_pubsub/testing.rb +157 -0
- data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
- data/lib/nats_pubsub/topology/stream.rb +102 -0
- data/lib/nats_pubsub/topology/stream_support.rb +170 -0
- data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
- data/lib/nats_pubsub/topology/topology.rb +24 -0
- data/lib/nats_pubsub/version.rb +8 -0
- data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
- data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
- data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
- data/lib/nats_pubsub/web/views/layout.erb +68 -0
- data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
- data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
- data/lib/nats_pubsub/web.rb +181 -0
- data/lib/nats_pubsub.rb +290 -0
- metadata +225 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module NatsPubsub
|
|
6
|
+
# Immutable value object representing a domain event or message.
|
|
7
|
+
# Provides a structured interface for accessing event envelope data.
|
|
8
|
+
#
|
|
9
|
+
# Events follow the envelope pattern with metadata and payload:
|
|
10
|
+
# - Metadata: event_id, timestamp, producer, domain/resource/action or topic
|
|
11
|
+
# - Payload: The actual event data
|
|
12
|
+
#
|
|
13
|
+
# @example Creating an event from envelope
|
|
14
|
+
# envelope = {
|
|
15
|
+
# 'event_id' => '123',
|
|
16
|
+
# 'domain' => 'users',
|
|
17
|
+
# 'resource' => 'user',
|
|
18
|
+
# 'action' => 'created',
|
|
19
|
+
# 'payload' => { 'id' => 1, 'name' => 'John' },
|
|
20
|
+
# 'occurred_at' => '2025-01-01T00:00:00Z'
|
|
21
|
+
# }
|
|
22
|
+
# event = Event.from_envelope(envelope)
|
|
23
|
+
# event.domain # => 'users'
|
|
24
|
+
# event.payload # => { 'id' => 1, 'name' => 'John' }
|
|
25
|
+
class Event
|
|
26
|
+
attr_reader :event_id, :schema_version, :payload, :occurred_at,
|
|
27
|
+
:trace_id, :producer, :resource_id
|
|
28
|
+
|
|
29
|
+
# Domain/Resource/Action fields (for event-based messages)
|
|
30
|
+
attr_reader :domain, :resource, :action
|
|
31
|
+
|
|
32
|
+
# Topic field (for topic-based messages)
|
|
33
|
+
attr_reader :topic, :message_type
|
|
34
|
+
|
|
35
|
+
# Create event from envelope hash
|
|
36
|
+
#
|
|
37
|
+
# @param envelope [Hash] Event envelope
|
|
38
|
+
# @return [Event] New event instance
|
|
39
|
+
# @raise [ArgumentError] if envelope is missing required fields
|
|
40
|
+
def self.from_envelope(envelope)
|
|
41
|
+
new(
|
|
42
|
+
event_id: envelope['event_id'] || envelope[:event_id],
|
|
43
|
+
schema_version: envelope['schema_version'] || envelope[:schema_version] || 1,
|
|
44
|
+
domain: envelope['domain'] || envelope[:domain],
|
|
45
|
+
resource: envelope['resource'] || envelope[:resource],
|
|
46
|
+
action: envelope['action'] || envelope[:action],
|
|
47
|
+
topic: envelope['topic'] || envelope[:topic],
|
|
48
|
+
message_type: envelope['message_type'] || envelope[:message_type],
|
|
49
|
+
producer: envelope['producer'] || envelope[:producer],
|
|
50
|
+
resource_id: envelope['resource_id'] || envelope[:resource_id],
|
|
51
|
+
occurred_at: parse_timestamp(envelope['occurred_at'] || envelope[:occurred_at]),
|
|
52
|
+
trace_id: envelope['trace_id'] || envelope[:trace_id],
|
|
53
|
+
payload: extract_payload(envelope)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Initialize a new Event
|
|
58
|
+
#
|
|
59
|
+
# @param event_id [String] Unique event identifier
|
|
60
|
+
# @param schema_version [Integer] Event schema version
|
|
61
|
+
# @param payload [Hash] Event payload data
|
|
62
|
+
# @param occurred_at [Time] When event occurred
|
|
63
|
+
# @param producer [String, nil] Application that produced the event
|
|
64
|
+
# @param trace_id [String, nil] Distributed tracing ID
|
|
65
|
+
# @param resource_id [String, nil] ID of the resource
|
|
66
|
+
# @param domain [String, nil] Domain name (event-based)
|
|
67
|
+
# @param resource [String, nil] Resource type (event-based)
|
|
68
|
+
# @param action [String, nil] Action performed (event-based)
|
|
69
|
+
# @param topic [String, nil] Topic name (topic-based)
|
|
70
|
+
# @param message_type [String, nil] Message type (topic-based)
|
|
71
|
+
def initialize(event_id:, schema_version: 1, payload:, occurred_at: nil,
|
|
72
|
+
producer: nil, trace_id: nil, resource_id: nil,
|
|
73
|
+
domain: nil, resource: nil, action: nil,
|
|
74
|
+
topic: nil, message_type: nil)
|
|
75
|
+
@event_id = event_id.to_s
|
|
76
|
+
@schema_version = schema_version.to_i
|
|
77
|
+
@payload = payload || {}
|
|
78
|
+
@occurred_at = occurred_at || Time.now.utc
|
|
79
|
+
@producer = producer
|
|
80
|
+
@trace_id = trace_id
|
|
81
|
+
@resource_id = resource_id
|
|
82
|
+
|
|
83
|
+
# Event-based fields
|
|
84
|
+
@domain = domain
|
|
85
|
+
@resource = resource
|
|
86
|
+
@action = action
|
|
87
|
+
|
|
88
|
+
# Topic-based fields
|
|
89
|
+
@topic = topic
|
|
90
|
+
@message_type = message_type
|
|
91
|
+
|
|
92
|
+
validate!
|
|
93
|
+
freeze
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Check if this is an event-based message
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean] True if has domain/resource/action
|
|
99
|
+
def event_based?
|
|
100
|
+
!@domain.nil? && !@resource.nil? && !@action.nil?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Check if this is a topic-based message
|
|
104
|
+
#
|
|
105
|
+
# @return [Boolean] True if has topic
|
|
106
|
+
def topic_based?
|
|
107
|
+
!@topic.nil?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Get event type identifier
|
|
111
|
+
# Returns domain.resource.action for events, or topic for topic messages
|
|
112
|
+
#
|
|
113
|
+
# @return [String] Event type
|
|
114
|
+
def event_type
|
|
115
|
+
if event_based?
|
|
116
|
+
"#{@domain}.#{@resource}.#{@action}"
|
|
117
|
+
elsif topic_based?
|
|
118
|
+
@topic
|
|
119
|
+
else
|
|
120
|
+
'unknown'
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check if event matches domain/resource/action pattern
|
|
125
|
+
#
|
|
126
|
+
# @param domain [String, nil] Domain to match (nil matches any)
|
|
127
|
+
# @param resource [String, nil] Resource to match (nil matches any)
|
|
128
|
+
# @param action [String, nil] Action to match (nil matches any)
|
|
129
|
+
# @return [Boolean] True if matches
|
|
130
|
+
def matches_event?(domain: nil, resource: nil, action: nil)
|
|
131
|
+
return false unless event_based?
|
|
132
|
+
|
|
133
|
+
(domain.nil? || @domain == domain.to_s) &&
|
|
134
|
+
(resource.nil? || @resource == resource.to_s) &&
|
|
135
|
+
(action.nil? || @action == action.to_s)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Check if event matches topic pattern
|
|
139
|
+
#
|
|
140
|
+
# @param topic [String] Topic to match
|
|
141
|
+
# @return [Boolean] True if matches
|
|
142
|
+
def matches_topic?(topic)
|
|
143
|
+
return false unless topic_based?
|
|
144
|
+
|
|
145
|
+
@topic == topic.to_s
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Convert event to envelope hash (for serialization)
|
|
149
|
+
#
|
|
150
|
+
# @return [Hash] Event envelope
|
|
151
|
+
def to_envelope
|
|
152
|
+
envelope = {
|
|
153
|
+
'event_id' => @event_id,
|
|
154
|
+
'schema_version' => @schema_version,
|
|
155
|
+
'occurred_at' => @occurred_at.iso8601,
|
|
156
|
+
'producer' => @producer,
|
|
157
|
+
'trace_id' => @trace_id
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if event_based?
|
|
161
|
+
envelope.merge!(
|
|
162
|
+
'domain' => @domain,
|
|
163
|
+
'resource' => @resource,
|
|
164
|
+
'action' => @action,
|
|
165
|
+
'resource_id' => @resource_id,
|
|
166
|
+
'payload' => @payload
|
|
167
|
+
)
|
|
168
|
+
elsif topic_based?
|
|
169
|
+
envelope.merge!(
|
|
170
|
+
'topic' => @topic,
|
|
171
|
+
'message_type' => @message_type,
|
|
172
|
+
'message' => @payload
|
|
173
|
+
)
|
|
174
|
+
else
|
|
175
|
+
envelope['payload'] = @payload
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
envelope.compact
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Convert to hash
|
|
182
|
+
#
|
|
183
|
+
# @return [Hash] Event as hash
|
|
184
|
+
def to_h
|
|
185
|
+
{
|
|
186
|
+
event_id: @event_id,
|
|
187
|
+
schema_version: @schema_version,
|
|
188
|
+
domain: @domain,
|
|
189
|
+
resource: @resource,
|
|
190
|
+
action: @action,
|
|
191
|
+
topic: @topic,
|
|
192
|
+
message_type: @message_type,
|
|
193
|
+
producer: @producer,
|
|
194
|
+
resource_id: @resource_id,
|
|
195
|
+
occurred_at: @occurred_at,
|
|
196
|
+
trace_id: @trace_id,
|
|
197
|
+
payload: @payload
|
|
198
|
+
}.compact
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# String representation
|
|
202
|
+
#
|
|
203
|
+
# @return [String] Event description
|
|
204
|
+
def to_s
|
|
205
|
+
"Event(#{event_type}, id=#{@event_id})"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Inspect representation
|
|
209
|
+
#
|
|
210
|
+
# @return [String] Detailed inspection
|
|
211
|
+
def inspect
|
|
212
|
+
"#<Event:#{event_type} id=#{@event_id} occurred_at=#{@occurred_at.iso8601}>"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Equality comparison
|
|
216
|
+
#
|
|
217
|
+
# @param other [Object] Object to compare
|
|
218
|
+
# @return [Boolean] True if equal
|
|
219
|
+
def ==(other)
|
|
220
|
+
other.is_a?(Event) && @event_id == other.event_id
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
alias eql? ==
|
|
224
|
+
|
|
225
|
+
# Hash code for use in hash tables
|
|
226
|
+
#
|
|
227
|
+
# @return [Integer] Hash code
|
|
228
|
+
def hash
|
|
229
|
+
@event_id.hash
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
private
|
|
233
|
+
|
|
234
|
+
# Parse timestamp string to Time object
|
|
235
|
+
#
|
|
236
|
+
# @param timestamp [String, Time, nil] Timestamp
|
|
237
|
+
# @return [Time] Parsed time
|
|
238
|
+
def self.parse_timestamp(timestamp)
|
|
239
|
+
case timestamp
|
|
240
|
+
when Time
|
|
241
|
+
timestamp
|
|
242
|
+
when String
|
|
243
|
+
Time.parse(timestamp)
|
|
244
|
+
else
|
|
245
|
+
Time.now.utc
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Extract payload from envelope
|
|
250
|
+
#
|
|
251
|
+
# @param envelope [Hash] Event envelope
|
|
252
|
+
# @return [Hash] Payload data
|
|
253
|
+
def self.extract_payload(envelope)
|
|
254
|
+
# For event-based messages, payload is in 'payload'
|
|
255
|
+
# For topic-based messages, payload is in 'message'
|
|
256
|
+
envelope['payload'] || envelope[:payload] ||
|
|
257
|
+
envelope['message'] || envelope[:message] ||
|
|
258
|
+
{}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Validate event data
|
|
262
|
+
#
|
|
263
|
+
# @raise [ArgumentError] if invalid
|
|
264
|
+
def validate!
|
|
265
|
+
raise ArgumentError, 'event_id is required' if @event_id.nil? || @event_id.empty?
|
|
266
|
+
raise ArgumentError, 'payload must be a Hash' unless @payload.is_a?(Hash)
|
|
267
|
+
raise ArgumentError, 'occurred_at must be a Time' unless @occurred_at.is_a?(Time)
|
|
268
|
+
|
|
269
|
+
# Must be either event-based or topic-based
|
|
270
|
+
unless event_based? || topic_based?
|
|
271
|
+
raise ArgumentError, 'Event must have either (domain, resource, action) or (topic)'
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|