event_engine 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +55 -0
- data/README.md +210 -399
- data/Rakefile +0 -1
- data/lib/event_engine/catalog_entry.rb +84 -0
- data/lib/event_engine/configuration.rb +9 -9
- data/lib/event_engine/definition_publisher.rb +22 -0
- data/lib/event_engine/event.rb +0 -8
- data/lib/event_engine/event_builder.rb +0 -8
- data/lib/event_engine/event_schema.rb +32 -57
- data/lib/event_engine/event_schema_json_loader.rb +19 -0
- data/lib/event_engine/handler_registry.rb +4 -4
- data/lib/event_engine/invalid_rules_error.rb +8 -0
- data/lib/event_engine/processing_rules.rb +39 -0
- data/lib/event_engine/processor_registry.rb +19 -0
- data/lib/event_engine/processor_resolver.rb +16 -0
- data/lib/event_engine/railtie.rb +54 -4
- data/lib/event_engine/rules_file.rb +14 -0
- data/lib/event_engine/schema_catalog_builder.rb +18 -0
- data/lib/event_engine/schema_registry.rb +7 -58
- data/lib/event_engine/unregistered_processor_error.rb +8 -0
- data/lib/event_engine/unroutable_event_error.rb +9 -0
- data/lib/event_engine/unrouted_events_error.rb +9 -0
- data/lib/event_engine/version.rb +1 -1
- data/lib/event_engine.rb +117 -123
- data/lib/tasks/event_engine_catalog.rake +15 -6
- data/lib/tasks/event_engine_rules.rake +10 -0
- data/the_local/agents/event_engine-develop.md +154 -0
- data/the_local/agents/event_engine-info.md +82 -0
- data/the_local/agents/event_engine-install.md +99 -0
- data/the_local/interface.yml +27 -0
- metadata +45 -48
- data/app/assets/config/event_engine_manifest.js +0 -1
- data/app/assets/stylesheets/event_engine/application.css +0 -15
- data/app/controllers/event_engine/application_controller.rb +0 -4
- data/app/helpers/event_engine/application_helper.rb +0 -4
- data/app/jobs/event_engine/application_job.rb +0 -4
- data/app/mailers/event_engine/application_mailer.rb +0 -6
- data/app/models/event_engine/application_record.rb +0 -5
- data/app/views/layouts/event_engine/application.html.erb +0 -15
- data/config/routes.rb +0 -2
- data/lib/event_engine/definition_loader.rb +0 -26
- data/lib/event_engine/dsl_compiler.rb +0 -50
- data/lib/event_engine/engine.rb +0 -56
- data/lib/event_engine/event_definition/inputs.rb +0 -43
- data/lib/event_engine/event_definition/payloads.rb +0 -47
- data/lib/event_engine/event_definition/schemas.rb +0 -158
- data/lib/event_engine/event_definition/validation.rb +0 -18
- data/lib/event_engine/event_definition.rb +0 -76
- data/lib/event_engine/event_schema_dumper.rb +0 -13
- data/lib/event_engine/event_schema_loader.rb +0 -37
- data/lib/event_engine/event_schema_merger.rb +0 -62
- data/lib/event_engine/event_schema_writer.rb +0 -47
- data/lib/event_engine/lifecycle_definition.rb +0 -86
- data/lib/event_engine/process_type.rb +0 -26
- data/lib/event_engine/reference/guide.md +0 -129
- data/lib/event_engine/reference.rb +0 -16
- data/lib/event_engine/schema_catalog.rb +0 -50
- data/lib/event_engine/schema_compatibility.rb +0 -50
- data/lib/event_engine/schema_diff.rb +0 -35
- data/lib/event_engine/schema_drift_guard.rb +0 -38
- data/lib/event_engine/subject_registry.rb +0 -40
- data/lib/event_engine/the_local/agents/event_engine-develop.md +0 -142
- data/lib/event_engine/the_local/agents/event_engine-info.md +0 -140
- data/lib/event_engine/the_local/agents/event_engine-install.md +0 -140
- data/lib/event_engine/the_local.rb +0 -55
- data/lib/generators/event_engine/install_generator.rb +0 -31
- data/lib/generators/event_engine/templates/event_schema.rb +0 -10
- data/lib/generators/event_engine/templates/initializer.rb +0 -4
- data/lib/tasks/event_engine_schema.rake +0 -82
- data/lib/tasks/event_engine_schema_check.rake +0 -20
- data/lib/tasks/event_engine_tasks.rake +0 -4
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class UnroutedEventsError < StandardError
|
|
3
|
+
def initialize(event_names)
|
|
4
|
+
super("no processing rule routes #{event_names.map(&:inspect).join(", ")}; " \
|
|
5
|
+
"declare #{event_names.one? ? "it" : "them"} in the rules file " \
|
|
6
|
+
"under events, packs, or default")
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
data/lib/event_engine/version.rb
CHANGED
data/lib/event_engine.rb
CHANGED
|
@@ -1,56 +1,31 @@
|
|
|
1
1
|
require_relative "event_engine/version"
|
|
2
2
|
|
|
3
|
-
require "event_engine/
|
|
3
|
+
require "event_engine/railtie"
|
|
4
4
|
require "event_engine/configuration"
|
|
5
|
-
require "event_engine/
|
|
6
|
-
require "event_engine/event_definition"
|
|
7
|
-
require "event_engine/lifecycle_definition"
|
|
5
|
+
require "event_engine/catalog_entry"
|
|
8
6
|
require "event_engine/event_builder"
|
|
7
|
+
require "event_engine/definition_publisher"
|
|
9
8
|
require "event_engine/handler_registry"
|
|
9
|
+
require "event_engine/processor_registry"
|
|
10
|
+
require "event_engine/unroutable_event_error"
|
|
11
|
+
require "event_engine/unregistered_processor_error"
|
|
12
|
+
require "event_engine/invalid_rules_error"
|
|
13
|
+
require "event_engine/unrouted_events_error"
|
|
14
|
+
require "event_engine/processor_resolver"
|
|
15
|
+
require "event_engine/processing_rules"
|
|
16
|
+
require "event_engine/rules_file"
|
|
10
17
|
require "event_engine/event_schema"
|
|
11
18
|
require "event_engine/schema_registry"
|
|
12
|
-
require "event_engine/subject_registry"
|
|
13
19
|
require "event_engine/event"
|
|
14
|
-
require "event_engine/
|
|
15
|
-
require "event_engine/
|
|
16
|
-
|
|
17
|
-
require "event_engine/event_schema_merger"
|
|
18
|
-
require "event_engine/event_schema_dumper"
|
|
19
|
-
require "event_engine/schema_drift_guard"
|
|
20
|
-
require "event_engine/schema_compatibility"
|
|
21
|
-
require "event_engine/schema_catalog"
|
|
22
|
-
require "event_engine/railtie"
|
|
23
|
-
require "event_engine/definition_loader"
|
|
24
|
-
require "event_engine/the_local"
|
|
25
|
-
|
|
26
|
-
# EventEngine is the schema-first core of the event pipeline.
|
|
27
|
-
#
|
|
28
|
-
# Events are defined via a Ruby DSL and compiled into a canonical schema file.
|
|
29
|
-
# At boot, a helper method is installed on this module for each registered event
|
|
30
|
-
# (e.g. +EventEngine.cow_fed(cow: cow)+); the helper validates inputs, builds an
|
|
31
|
-
# +Event+, and dispatches it to registered handlers by level. Companion gems
|
|
32
|
-
# (e.g. event_engine-delivery) register handlers to process the events.
|
|
33
|
-
#
|
|
34
|
-
# @example Define, build, and dispatch an event
|
|
35
|
-
# EventEngine.register_handler(MyHandler, levels: :all)
|
|
36
|
-
# EventEngine.cow_fed(cow: cow, occurred_at: Time.current)
|
|
20
|
+
require "event_engine/event_schema_json_loader"
|
|
21
|
+
require "event_engine/schema_catalog_builder"
|
|
22
|
+
|
|
37
23
|
module EventEngine
|
|
38
|
-
mattr_accessor :_installed_event_helpers, default: Set.new
|
|
39
24
|
class << self
|
|
40
|
-
# Returns the current configuration instance.
|
|
41
|
-
#
|
|
42
|
-
# @return [Configuration]
|
|
43
25
|
def configuration
|
|
44
26
|
@configuration ||= Configuration.new
|
|
45
27
|
end
|
|
46
28
|
|
|
47
|
-
# Yields the configuration for modification.
|
|
48
|
-
#
|
|
49
|
-
# @yieldparam config [Configuration] the configuration instance
|
|
50
|
-
# @example
|
|
51
|
-
# EventEngine.configure do |config|
|
|
52
|
-
# config.logger = Rails.logger
|
|
53
|
-
# end
|
|
54
29
|
def configure
|
|
55
30
|
yield(configuration)
|
|
56
31
|
end
|
|
@@ -59,16 +34,81 @@ module EventEngine
|
|
|
59
34
|
@handler_registry ||= HandlerRegistry.new
|
|
60
35
|
end
|
|
61
36
|
|
|
62
|
-
def
|
|
63
|
-
@
|
|
37
|
+
def processor_registry
|
|
38
|
+
@processor_registry ||= ProcessorRegistry.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def schema_registry
|
|
42
|
+
@schema_registry ||= SchemaRegistry.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
attr_writer :schema_registry
|
|
46
|
+
|
|
47
|
+
def emit(event_name, inputs:, domain: nil, event_version: nil, occurred_at: nil,
|
|
48
|
+
metadata: nil, idempotency_key: nil, aggregate_type: nil,
|
|
49
|
+
aggregate_id: nil, aggregate_version: nil)
|
|
50
|
+
schema = schema_registry.schema(event_name, version: event_version, domain: domain)
|
|
51
|
+
|
|
52
|
+
attrs = EventBuilder.build(schema: schema, data: inputs)
|
|
53
|
+
attrs[:occurred_at] = occurred_at || Time.current
|
|
54
|
+
attrs[:metadata] = enriched_metadata(metadata)
|
|
55
|
+
attrs[:idempotency_key] = idempotency_key || SecureRandom.uuid
|
|
56
|
+
attrs[:aggregate_type] = aggregate_type
|
|
57
|
+
attrs[:aggregate_id] = aggregate_id
|
|
58
|
+
attrs[:aggregate_version] = aggregate_version
|
|
59
|
+
attrs[:process_type] = processing_rules.for(event_name: schema.event_name, pack: schema.domain)
|
|
60
|
+
attrs[:subject] = schema.subject
|
|
61
|
+
attrs[:domain] = schema.domain
|
|
62
|
+
|
|
63
|
+
event = Event.new(**attrs)
|
|
64
|
+
process(event)
|
|
65
|
+
dispatch(event)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def processing_rules
|
|
69
|
+
@processing_rules ||= ProcessingRules.load(configuration.rules_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
attr_writer :processing_rules
|
|
73
|
+
|
|
74
|
+
def validate_rules!
|
|
75
|
+
missing = processing_rules.processor_names.reject { |name| processor_registry.fetch(name) }
|
|
76
|
+
raise InvalidRulesError, missing if missing.any?
|
|
77
|
+
|
|
78
|
+
raise UnroutedEventsError, unrouted_events if unrouted_events.any?
|
|
79
|
+
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def unrouted_events
|
|
84
|
+
return [] unless processing_rules.any?
|
|
85
|
+
|
|
86
|
+
schema_registry.events.reject do |event_name|
|
|
87
|
+
processing_rules.for(event_name: event_name, pack: schema_registry.latest_for(event_name).domain)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def schema_sources(port = definition_port)
|
|
92
|
+
configured = configuration.publisher_schema_paths
|
|
93
|
+
return configured if configured.any?
|
|
94
|
+
|
|
95
|
+
discovered_schema_paths(port)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def discovered_schema_paths(port = definition_port)
|
|
99
|
+
return [] unless port.respond_to?(:pack_schema_paths)
|
|
100
|
+
|
|
101
|
+
port.pack_schema_paths
|
|
64
102
|
end
|
|
65
103
|
|
|
66
|
-
def
|
|
67
|
-
|
|
104
|
+
def register_definition_publisher!(port = definition_port)
|
|
105
|
+
return nil unless port.respond_to?(:publisher=)
|
|
106
|
+
|
|
107
|
+
port.publisher = DefinitionPublisher.new
|
|
68
108
|
end
|
|
69
109
|
|
|
70
|
-
def
|
|
71
|
-
|
|
110
|
+
def definition_port
|
|
111
|
+
::EventEngine::Definition if defined?(::EventEngine::Definition)
|
|
72
112
|
end
|
|
73
113
|
|
|
74
114
|
def enriched_metadata(call_site_metadata)
|
|
@@ -88,107 +128,61 @@ module EventEngine
|
|
|
88
128
|
nil
|
|
89
129
|
end
|
|
90
130
|
|
|
91
|
-
def register_handler(handler,
|
|
92
|
-
handler_registry.register(handler,
|
|
131
|
+
def register_handler(handler, process_types:)
|
|
132
|
+
handler_registry.register(handler, process_types: process_types)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def register_processor(name, processor)
|
|
136
|
+
processor_registry.register(name, processor)
|
|
93
137
|
end
|
|
94
138
|
|
|
95
139
|
def dispatch(event)
|
|
96
140
|
handler_registry.dispatch(event)
|
|
97
141
|
end
|
|
98
142
|
|
|
143
|
+
def process(event)
|
|
144
|
+
resolver = ProcessorResolver.new(processing_rules)
|
|
145
|
+
return event unless resolver.routes?
|
|
146
|
+
|
|
147
|
+
name = resolver.resolve(event)
|
|
148
|
+
processor = processor_registry.fetch(name) || raise(UnregisteredProcessorError.new(name, event))
|
|
149
|
+
|
|
150
|
+
processor.call(event)
|
|
151
|
+
event
|
|
152
|
+
end
|
|
153
|
+
|
|
99
154
|
def reset_handlers!
|
|
100
155
|
handler_registry.clear!
|
|
101
156
|
end
|
|
102
157
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# @param registry [SchemaRegistry] the registry to populate
|
|
108
|
-
# @return [EventSchema] the loaded schema
|
|
158
|
+
def reset_processors!
|
|
159
|
+
processor_registry.clear!
|
|
160
|
+
end
|
|
161
|
+
|
|
109
162
|
def boot_from_schema!(schema_path:, registry:)
|
|
110
|
-
event_schema =
|
|
163
|
+
event_schema = EventSchemaJsonLoader.load(schema_path)
|
|
111
164
|
|
|
112
165
|
registry.reset!
|
|
113
166
|
registry.load_from_schema!(event_schema)
|
|
114
167
|
|
|
115
|
-
|
|
168
|
+
self.schema_registry = registry
|
|
116
169
|
|
|
117
170
|
event_schema
|
|
118
171
|
end
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
singleton_class.remove_method(method_name) if singleton_class.method_defined?(method_name)
|
|
127
|
-
end
|
|
128
|
-
_installed_event_helpers.clear
|
|
129
|
-
|
|
130
|
-
registry.events.each do |event_name|
|
|
131
|
-
schema = registry.schema(event_name)
|
|
132
|
-
|
|
133
|
-
required = schema.required_inputs
|
|
134
|
-
optional = schema.optional_inputs
|
|
135
|
-
|
|
136
|
-
define_singleton_method(event_name) do |**args|
|
|
137
|
-
event_version = args.delete(:event_version)
|
|
138
|
-
occurred_at = args.delete(:occurred_at)
|
|
139
|
-
metadata = args.delete(:metadata)
|
|
140
|
-
idempotency_key = args.delete(:idempotency_key)
|
|
141
|
-
aggregate_type = args.delete(:aggregate_type)
|
|
142
|
-
aggregate_id = args.delete(:aggregate_id)
|
|
143
|
-
aggregate_version = args.delete(:aggregate_version)
|
|
144
|
-
|
|
145
|
-
input_keys = required + optional
|
|
146
|
-
inputs = args.slice(*input_keys)
|
|
147
|
-
|
|
148
|
-
missing = required - inputs.keys
|
|
149
|
-
raise ArgumentError, "Missing required inputs: #{missing.join(', ')}" if missing.any?
|
|
150
|
-
|
|
151
|
-
unknown = args.keys - input_keys
|
|
152
|
-
raise ArgumentError, "Unknown inputs: #{unknown.join(', ')}" if unknown.any?
|
|
153
|
-
|
|
154
|
-
schema = registry.schema(event_name, version: event_version)
|
|
155
|
-
attrs = EventBuilder.build(schema: schema, data: inputs)
|
|
156
|
-
attrs[:occurred_at] = occurred_at || Time.current
|
|
157
|
-
attrs[:metadata] = EventEngine.enriched_metadata(metadata)
|
|
158
|
-
attrs[:idempotency_key] = idempotency_key || SecureRandom.uuid
|
|
159
|
-
attrs[:aggregate_type] = aggregate_type
|
|
160
|
-
attrs[:aggregate_id] = aggregate_id
|
|
161
|
-
attrs[:aggregate_version] = aggregate_version
|
|
162
|
-
attrs[:process_type] = schema.process_type
|
|
163
|
-
attrs[:subject] = schema.subject
|
|
164
|
-
attrs[:domain] = schema.domain
|
|
165
|
-
|
|
166
|
-
EventEngine.dispatch(Event.new(**attrs))
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
_installed_event_helpers << event_name
|
|
173
|
+
def register_slice!(schema_path:)
|
|
174
|
+
slice = EventSchemaJsonLoader.load(schema_path)
|
|
175
|
+
|
|
176
|
+
schema_registry.load_from_schema!(EventSchema.new) unless schema_registry.loaded?
|
|
177
|
+
slice.event_schema.schemas_by_event.each_value do |versions|
|
|
178
|
+
versions.each_value { |schema| schema_registry.register(schema) }
|
|
170
179
|
end
|
|
171
|
-
end
|
|
172
180
|
|
|
173
|
-
|
|
174
|
-
# Used by rake tasks for schema drift detection.
|
|
175
|
-
#
|
|
176
|
-
# @return [SchemaRegistry]
|
|
177
|
-
def compiled_schema_registry
|
|
178
|
-
DefinitionLoader.ensure_loaded!
|
|
179
|
-
definitions = EventDefinition.descendants
|
|
180
|
-
compiled = DslCompiler.compile(definitions)
|
|
181
|
-
registry = SchemaRegistry.new
|
|
182
|
-
registry.load_from_schema!(compiled)
|
|
183
|
-
registry
|
|
181
|
+
schema_registry
|
|
184
182
|
end
|
|
185
183
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
#
|
|
189
|
-
# @return [SchemaRegistry]
|
|
190
|
-
def file_schema_registry
|
|
191
|
-
loaded = EventSchemaLoader.load(Rails.root.join("db/event_schema.rb"))
|
|
184
|
+
def file_schema_registry(schema_path: configuration.schema_path)
|
|
185
|
+
loaded = EventSchemaJsonLoader.load(schema_path)
|
|
192
186
|
registry = SchemaRegistry.new
|
|
193
187
|
registry.load_from_schema!(loaded)
|
|
194
188
|
registry
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
namespace :event_engine do
|
|
2
|
-
desc "
|
|
2
|
+
desc "Build the committed catalog from every pack, and keep the rules file in step"
|
|
3
3
|
task catalog: :environment do
|
|
4
|
-
EventEngine
|
|
4
|
+
catalog_path = Rails.root.join(EventEngine.configuration.schema_path)
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
EventEngine::SchemaCatalogBuilder.build(
|
|
7
|
+
sources: EventEngine.schema_sources,
|
|
8
|
+
catalog_path: catalog_path
|
|
9
9
|
)
|
|
10
10
|
|
|
11
|
-
puts catalog
|
|
11
|
+
puts "Wrote EventEngine catalog to #{catalog_path}"
|
|
12
|
+
|
|
13
|
+
rules_path = Rails.root.join(EventEngine.configuration.rules_path)
|
|
14
|
+
|
|
15
|
+
EventEngine::RulesFile.sync(
|
|
16
|
+
path: rules_path,
|
|
17
|
+
event_names: JSON.parse(File.read(catalog_path)).map { |entry| entry["event_name"] }
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
puts "Wrote EventEngine processing rules to #{rules_path}"
|
|
12
21
|
end
|
|
13
22
|
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
namespace :event_engine do
|
|
2
|
+
namespace :rules do
|
|
3
|
+
desc "Check that every processing rule names a registered processor"
|
|
4
|
+
task check: :environment do
|
|
5
|
+
EventEngine.validate_rules!
|
|
6
|
+
|
|
7
|
+
puts "EventEngine rules OK: #{EventEngine.processing_rules.processor_names.map(&:inspect).join(", ")}"
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-develop
|
|
3
|
+
description: Use PROACTIVELY for wiring an app's event processing on event_engine — registering processors and handlers, routing events to the right processor, building the committed schema catalog from event packs, and printing the catalog — MUST BE USED instead of hand-rolling event dispatch, a processor lookup table, or a script that stitches pack schemas together.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: events — registering processors and publishers, building the schema catalog, and directing emitted events to the right processor
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local wires a host app's event processing on event_engine, following the
|
|
9
|
+
steps below in order. Where a step names a decision, it asks the developer
|
|
10
|
+
instead of choosing.
|
|
11
|
+
|
|
12
|
+
## What event_engine is
|
|
13
|
+
|
|
14
|
+
A Rails engine that loads a committed catalog of event schemas at boot, turns an
|
|
15
|
+
event pack's raw inputs into a checked event, sends that event to exactly one
|
|
16
|
+
**processor**, and then to every matching **handler**. Fire this local when an app
|
|
17
|
+
needs its events built, routed, or catalogued — anything from "our pack's events
|
|
18
|
+
aren't reaching the subscriber" to "add the new pack's schema to the app".
|
|
19
|
+
|
|
20
|
+
## Interface
|
|
21
|
+
|
|
22
|
+
- `publisher_schema_paths` — configuration setting; the list of pack `schema.json`
|
|
23
|
+
files the catalog is built from. Defaults to empty.
|
|
24
|
+
- `bin/rails event_engine:schema:catalog` — reads every `publisher_schema_paths`
|
|
25
|
+
source and writes them into the app's committed catalog at `db/event_schema.json`.
|
|
26
|
+
- `EventEngine.register_processor` — registers a processor under a name, so a
|
|
27
|
+
routing setting can point at that name.
|
|
28
|
+
- `event_processors` — configuration setting; a hash of event name → processor
|
|
29
|
+
name. The most specific routing rule.
|
|
30
|
+
- `domain_processors` — configuration setting; a hash of domain → processor name.
|
|
31
|
+
Applies to every event in that domain.
|
|
32
|
+
- `default_processor` — configuration setting; the processor name used when no
|
|
33
|
+
event or domain rule matches.
|
|
34
|
+
- `EventEngine.register_handler` — registers a handler that runs after the
|
|
35
|
+
processor, filtered by the event's process type.
|
|
36
|
+
- `EventEngine.register_definition_publisher!` — points a pack's publisher port at
|
|
37
|
+
event_engine, so the pack's helpers produce real events. Runs automatically at
|
|
38
|
+
boot; call it directly only for a port other than the default.
|
|
39
|
+
- `bin/rails event_engine:catalog` — prints the catalogued events as markdown.
|
|
40
|
+
|
|
41
|
+
## How to use it
|
|
42
|
+
|
|
43
|
+
All configuration below goes in the host app's
|
|
44
|
+
`config/initializers/event_engine.rb`, on the configuration object yielded inside
|
|
45
|
+
it. The `event_engine-install` local creates that file — if it is missing, hand
|
|
46
|
+
off to that local first and come back.
|
|
47
|
+
|
|
48
|
+
1. Set `publisher_schema_paths` to the `schema.json` of every pack whose events
|
|
49
|
+
this app handles. Ask the developer which packs are in scope; do not infer the
|
|
50
|
+
list from the Gemfile, and do not guess a path.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
config.publisher_schema_paths = [
|
|
54
|
+
MarketingEvents.schema_path,
|
|
55
|
+
SalesEvents.schema_path
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Leave this unset and step 2 writes an empty catalog — no event is emittable.
|
|
60
|
+
|
|
61
|
+
2. Build the catalog and commit it:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
bin/rails event_engine:schema:catalog
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
It writes `db/event_schema.json` from the sources, in order, overwriting the
|
|
68
|
+
file. It never edits a source, and it never recomputes a schema's fingerprint —
|
|
69
|
+
what a pack published is what lands. Commit the result; the app reads that file
|
|
70
|
+
at boot, warns in `development` and `test` when it is absent, and refuses to
|
|
71
|
+
boot in every other environment. Re-run it whenever a pack's schema changes or
|
|
72
|
+
a pack is added to step 1.
|
|
73
|
+
|
|
74
|
+
3. Register each processor by name, in the initializer, so the names exist before
|
|
75
|
+
any event is emitted. A processor is anything responding to `#call(event)`:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
EventEngine.register_processor(:subscribers, MyApp::SubscriberProcessor)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Registering the same name twice replaces the earlier one.
|
|
82
|
+
|
|
83
|
+
4. Route events to those processors with the three settings. Ask the developer how
|
|
84
|
+
their events should route — there is no safe default, and the right answer
|
|
85
|
+
depends on which processors the app actually runs:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
config.default_processor = :subscribers
|
|
89
|
+
config.domain_processors = { marketing: :delivery }
|
|
90
|
+
config.event_processors = { lead_created: :telemetry }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Resolution is `event_processors[event_name]`, then
|
|
94
|
+
`domain_processors[domain]`, then `default_processor` — first match wins.
|
|
95
|
+
Every name used here must be registered in step 3.
|
|
96
|
+
|
|
97
|
+
5. Register handlers. A handler is anything responding to `#call(event)`, and it
|
|
98
|
+
runs after the processor. `process_types:` is required — either `:all`, or a
|
|
99
|
+
list of process types that is matched against the event's own process type:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
EventEngine.register_handler(MyApp::AuditLog, process_types: %i[durable broker])
|
|
103
|
+
EventEngine.register_handler(MyApp::Firehose, process_types: :all)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Every handler whose filter matches runs, in registration order. An event with
|
|
107
|
+
no matching handler is still built and processed.
|
|
108
|
+
|
|
109
|
+
6. Leave the publisher port alone unless the app has one of its own. At boot the
|
|
110
|
+
engine points the default port at event_engine, which is what makes a pack's
|
|
111
|
+
generated helper produce a real event instead of raising. Call it directly only
|
|
112
|
+
to install it on a different port object:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
EventEngine.register_definition_publisher!(MyApp::CustomPort)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
It is a no-op returning `nil` when the port cannot accept a publisher — which
|
|
119
|
+
is also what happens when no pack is loaded.
|
|
120
|
+
|
|
121
|
+
7. Verify the wiring:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bin/rails event_engine:catalog
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
It prints one markdown section per catalogued event — name, version, type,
|
|
128
|
+
subject, payload fields — read from the committed catalog. An event missing
|
|
129
|
+
here will not emit; go back to step 1.
|
|
130
|
+
|
|
131
|
+
## Conventions
|
|
132
|
+
|
|
133
|
+
- Both processors and handlers receive the same built event, carrying
|
|
134
|
+
`event_name`, `event_type`, `event_version`, `process_type`, `subject`,
|
|
135
|
+
`domain`, `payload`, `metadata`, `occurred_at`, `idempotency_key`, and the
|
|
136
|
+
`aggregate_type` / `aggregate_id` / `aggregate_version` trio. Read from it; do
|
|
137
|
+
not mutate it.
|
|
138
|
+
- The process types a handler filter can name are `inline`, `background`,
|
|
139
|
+
`durable`, `broker`, `telemetry`, and `sourced`. A schema declares one; the
|
|
140
|
+
filter is matched literally, so a typo silently never fires.
|
|
141
|
+
- With none of the three routing settings set, no processor runs at all — events
|
|
142
|
+
are built and go straight to handlers. Once **any** of them is set, an event
|
|
143
|
+
matching none of the rules raises `EventEngine::UnroutableEventError` at emit.
|
|
144
|
+
Setting only `domain_processors` for one domain therefore breaks every other
|
|
145
|
+
domain; pair narrow rules with a `default_processor`.
|
|
146
|
+
- Routing at a name with no registered processor fails at emit time, not at boot.
|
|
147
|
+
Keep steps 3 and 4 in the same initializer so they cannot drift.
|
|
148
|
+
- A pack helper firing an event that is not in the committed catalog raises
|
|
149
|
+
`EventEngine::DefinitionPublisher::EventNotInCatalogError`. That means the
|
|
150
|
+
catalog is stale — rebuild it in step 2, do not work around it in app code.
|
|
151
|
+
- Initializer changes need an app restart; nothing is re-read at runtime.
|
|
152
|
+
- Out of scope for this local: adding the gem and writing the initializer itself
|
|
153
|
+
(`event_engine-install`), and authoring event definitions or packs — this local
|
|
154
|
+
consumes a pack's published `schema.json`, it never writes one.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-info
|
|
3
|
+
description: Use to learn what event_engine offers — events, domains and subjects, process types, processors and handlers, and the schema catalog the rest of the gem reads from.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: events — registering processors and publishers, building the schema catalog, and directing emitted events to the right processor
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains event_engine and the vocabulary its world is built on. It
|
|
9
|
+
changes nothing and gives no steps — read it to orient, then go to the local that
|
|
10
|
+
owns the work.
|
|
11
|
+
|
|
12
|
+
## What event_engine is
|
|
13
|
+
|
|
14
|
+
event_engine is a Rails engine for applications that want events as a first-class
|
|
15
|
+
seam rather than a pile of callbacks. Each event is described by a schema — a
|
|
16
|
+
name, a version, the domain it belongs to, its subject, and the shape of its
|
|
17
|
+
payload. Those schemas are aggregated into one committed catalog that the app
|
|
18
|
+
loads at boot, and the catalog is authoritative: an event the catalog doesn't
|
|
19
|
+
know cannot be emitted. Emitting is therefore a checked operation, and the set of
|
|
20
|
+
events a system can produce is reviewable in a single file.
|
|
21
|
+
|
|
22
|
+
Reach for it when several parts of an app need to react to the same domain
|
|
23
|
+
occurrence, when you want the contract for those occurrences written down and
|
|
24
|
+
diffable, or when different classes of event need to travel different roads —
|
|
25
|
+
some handled in the request, some queued, some pushed to a broker, some recorded
|
|
26
|
+
for telemetry or sourcing. An emitted event is checked against the catalog,
|
|
27
|
+
routed to exactly one processor for that road, and then dispatched to any
|
|
28
|
+
handlers subscribed to its process type. Routing is opt-in: configure nothing and
|
|
29
|
+
events skip processing and go straight to handlers; configure any routing at all
|
|
30
|
+
and an event with no matching processor is an error rather than a silent drop.
|
|
31
|
+
|
|
32
|
+
## Interface
|
|
33
|
+
|
|
34
|
+
This local documents no commands — it is background only.
|
|
35
|
+
|
|
36
|
+
The `event_engine-install` local owns hooking the engine into a host app and
|
|
37
|
+
configuring it. The `event_engine-develop` local owns the working surface:
|
|
38
|
+
registering processors, publishers and handlers, pointing the engine at the
|
|
39
|
+
schema sources it should aggregate, choosing how events route, and the rake tasks
|
|
40
|
+
that build and render the catalog. Everything you would actually call or run
|
|
41
|
+
lives in one of those two.
|
|
42
|
+
|
|
43
|
+
## How to use it
|
|
44
|
+
|
|
45
|
+
One decision: are you wiring event_engine into an app for the first time, or
|
|
46
|
+
building with it?
|
|
47
|
+
|
|
48
|
+
- Standing the engine up in a host app, or changing how it is configured →
|
|
49
|
+
`event_engine-install`.
|
|
50
|
+
- Adding events, registering processors or handlers, setting up routing,
|
|
51
|
+
publishing a pack's schema, or rebuilding the catalog → `event_engine-develop`.
|
|
52
|
+
|
|
53
|
+
If you only needed the vocabulary, you have it — stop here.
|
|
54
|
+
|
|
55
|
+
## Conventions
|
|
56
|
+
|
|
57
|
+
- **Event** — one occurrence, identified by an `event_name` and an
|
|
58
|
+
`event_version`. Carries a payload plus an envelope: metadata, the time it
|
|
59
|
+
occurred, an idempotency key, and optional aggregate identity
|
|
60
|
+
(`aggregate_type`, `aggregate_id`, `aggregate_version`).
|
|
61
|
+
- **Domain** — the bounded area an event belongs to. Event names are looked up
|
|
62
|
+
within a domain, so the same name may exist in more than one.
|
|
63
|
+
- **Subject** — the thing an event is about, declared in its own registry with
|
|
64
|
+
metadata. Subjects are named vocabulary shared across events, not per-event
|
|
65
|
+
fields.
|
|
66
|
+
- **Process type** — how an event should travel: `inline`, `background`,
|
|
67
|
+
`durable`, `broker`, `telemetry`, or `sourced`. It comes from the schema, not
|
|
68
|
+
the call site.
|
|
69
|
+
- **Processor** — the single destination an event is routed to. Resolution is
|
|
70
|
+
most-specific-first: a processor bound to the event name, then one bound to the
|
|
71
|
+
domain, then the configured default.
|
|
72
|
+
- **Handler** — a subscriber that runs after processing. Handlers are registered
|
|
73
|
+
against process types (or all of them), and every match runs. Many handlers per
|
|
74
|
+
event; one processor.
|
|
75
|
+
- **Catalog** — the committed JSON file the app boots from, aggregated from each
|
|
76
|
+
publishing pack's own `schema.json`. Packs own their schemas; the catalog is
|
|
77
|
+
the assembled whole. If it is missing, development and test warn, other
|
|
78
|
+
environments refuse to boot.
|
|
79
|
+
- **Publisher** — the seam a pack emits through, so a pack depends on a small
|
|
80
|
+
publishing port rather than on the engine's internals.
|
|
81
|
+
- **Version** — omit one when emitting and the latest schema for that event wins;
|
|
82
|
+
name one to pin.
|