event_engine 0.1.0 → 0.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +218 -398
- 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
data/README.md
CHANGED
|
@@ -1,517 +1,337 @@
|
|
|
1
1
|
# EventEngine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **Rails host runtime** of the [EventEngine](https://github.com/DYB-Development) pipeline.
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
|
|
5
|
+
EventEngine is a schema-first event pipeline. Domain events are **declared** and
|
|
6
|
+
**compiled** into a committed contract by domain-pack gems (built on
|
|
7
|
+
[`event_engine-event_definition`](https://github.com/DYB-Development/event_engine-event_definition)).
|
|
8
|
+
This gem is the **runtime** a Rails app installs: it holds the schema registry,
|
|
9
|
+
**builds** a validated `EventEngine::Event` from the inputs a pack hands it, and
|
|
10
|
+
hands it to the **processor** its rules name.
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
and
|
|
12
|
-
|
|
12
|
+
This gem does **not** author events (that is `event_engine-event_definition` + your
|
|
13
|
+
domain packs) and does **not** deliver them (that is the processor gems below). It is
|
|
14
|
+
the middle of the pipe: inputs in, built event out, routed to a processor.
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
You can run the core gem **by itself** with your own handlers — see
|
|
21
|
-
[The handler extension point](#the-handler-extension-point).
|
|
22
|
-
|
|
23
|
-
> This README documents the core gem **only**. Outbox, transports, dead-letters,
|
|
24
|
-
> the dashboard, and the cloud reporter are documented in `event_engine-delivery`.
|
|
25
|
-
> The event log, replay, and projections are documented in `event_engine-store`.
|
|
26
|
-
|
|
27
|
-
---
|
|
28
|
-
|
|
29
|
-
## Table of contents
|
|
30
|
-
|
|
31
|
-
- [Quick start](#quick-start)
|
|
32
|
-
- [Mental model](#mental-model)
|
|
33
|
-
- [Defining events](#defining-events)
|
|
34
|
-
- [The DSL reference](#the-dsl-reference)
|
|
35
|
-
- [How payload fields are extracted](#how-payload-fields-are-extracted)
|
|
36
|
-
- [There is no `type:` casting](#there-is-no-type-casting)
|
|
37
|
-
- [Lifecycle event families](#lifecycle-event-families)
|
|
38
|
-
- [Generating the schema](#generating-the-schema)
|
|
39
|
-
- [How versioning works](#how-versioning-works)
|
|
40
|
-
- [Drift checking in CI](#drift-checking-in-ci)
|
|
41
|
-
- [Emitting events](#emitting-events)
|
|
42
|
-
- [Subscribers](#subscribers)
|
|
43
|
-
- [Event levels](#event-levels)
|
|
44
|
-
- [The handler extension point](#the-handler-extension-point)
|
|
45
|
-
- [Configuration](#configuration)
|
|
46
|
-
- [Rake tasks](#rake-tasks)
|
|
47
|
-
- [Installation generator](#installation-generator)
|
|
48
|
-
- [For AI assistants](#for-ai-assistants)
|
|
49
|
-
- [Contributing](#contributing)
|
|
50
|
-
- [License](#license)
|
|
16
|
+
> **Status.** The authoring layer is fully extracted out of this gem, and the
|
|
17
|
+
> passive wiring is in place: the publisher adapter registers itself at boot and
|
|
18
|
+
> packs are discovered without per-pack host config. A host registers its
|
|
19
|
+
> processors and declares its [rules file](#declaring-how-events-are-processed) —
|
|
20
|
+
> nothing else.
|
|
51
21
|
|
|
52
22
|
---
|
|
53
23
|
|
|
54
|
-
##
|
|
55
|
-
|
|
56
|
-
```ruby
|
|
57
|
-
# Gemfile
|
|
58
|
-
gem "event_engine"
|
|
59
|
-
```
|
|
24
|
+
## Where this gem sits
|
|
60
25
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
26
|
+
| Gem | Responsibility | Add it when |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| [`event_engine-event_definition`](https://github.com/DYB-Development/event_engine-event_definition) | The DSL, schema value objects, and the build step that turns definitions into a committed helper file + `schema.json`. No Rails. | A gem or app **declares** events |
|
|
29
|
+
| **`event_engine`** (this gem) | Catalog, build a validated event, route it to the processor its rules name | Always — it's the host runtime |
|
|
30
|
+
| `event_engine-delivery` | Transactional outbox, retries, dead-letters, transports (Kafka), dashboard | You need durable/broker delivery |
|
|
31
|
+
| `event_engine-store` | Durable append-only event log + replay & projections | You need a permanent record / event sourcing |
|
|
32
|
+
| `event_engine-subscribers`, `-telemetry`, `-sourced` | Processor gems for inline/background subscribers, telemetry, and event sourcing | You want that processing style |
|
|
64
33
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
bin/rails event_engine:schema:dump
|
|
69
|
-
```
|
|
70
|
-
3. **Commit `db/event_schema.rb`** — it is authoritative at runtime.
|
|
71
|
-
4. **Register at least one handler** so emitted events do something. Either add a
|
|
72
|
-
companion gem (`event_engine-delivery` / `event_engine-store`) or write your own
|
|
73
|
-
(see [The handler extension point](#the-handler-extension-point)).
|
|
74
|
-
5. **Emit** from your app code:
|
|
75
|
-
```ruby
|
|
76
|
-
EventEngine.cow_fed(cow: cow)
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
> With **no** handler registered, the core gem builds and dispatches the event but
|
|
80
|
-
> nothing observes it. That's expected — core is the dispatch layer; handlers are
|
|
81
|
-
> what *do* something with events.
|
|
34
|
+
Domain packs (e.g. `event_engine-marketing_events`, `-sales_events`, `-user_events`)
|
|
35
|
+
declare events against `event_engine-event_definition` and each ships its own
|
|
36
|
+
committed `schema.json` and helper module.
|
|
82
37
|
|
|
83
38
|
---
|
|
84
39
|
|
|
85
|
-
##
|
|
40
|
+
## How it fits together
|
|
41
|
+
|
|
42
|
+
A pack's generated helper does **not** emit. It forwards the raw inputs through a
|
|
43
|
+
**publisher port**; this gem is what turns those inputs into a real event and
|
|
44
|
+
routes it.
|
|
86
45
|
|
|
87
46
|
```
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
47
|
+
pack helper (event_engine-event_definition)
|
|
48
|
+
MarketingEvents.lead_created(lead: lead, …envelope)
|
|
49
|
+
│ calls the publisher PORT (not emit):
|
|
50
|
+
▼
|
|
51
|
+
EventEngine::Definition.publisher.publish(:lead_created, domain: :marketing,
|
|
52
|
+
inputs: { lead: lead }, …envelope)
|
|
53
|
+
▼
|
|
54
|
+
EventEngine::DefinitionPublisher ◄── registered at boot, no host wiring
|
|
55
|
+
│ EventEngine.emit(:lead_created, inputs: { lead: lead }, domain: :marketing, …)
|
|
56
|
+
▼
|
|
57
|
+
EventBuilder builds the payload from each field's from:/attr: ◄── catalog entry
|
|
58
|
+
│ the rules file decides which processor handles it
|
|
59
|
+
▼
|
|
60
|
+
the processor named by the rule
|
|
61
|
+
(event_engine-delivery / -store / -subscribers / your own)
|
|
102
62
|
```
|
|
103
63
|
|
|
104
|
-
|
|
64
|
+
**`EventEngine.emit` is the official emit, and it lives here** — not in the pack. The
|
|
65
|
+
pack passes *inputs* (the whole objects you already have); this gem reads them via
|
|
66
|
+
the catalog entry's `from:`/`attr:` mapping to build the flat `payload`, stamps the
|
|
67
|
+
envelope, and routes it.
|
|
105
68
|
|
|
106
|
-
|
|
107
|
-
truth at runtime.** Definition classes are read only at *dump* time. In
|
|
108
|
-
production a missing `db/event_schema.rb` raises at boot.
|
|
109
|
-
2. **Emitting and handling are decoupled.** `EventEngine.dispatch` just fans the
|
|
110
|
-
event out to handlers by level. The core gem ships *no* handlers.
|
|
69
|
+
### The passive wiring
|
|
111
70
|
|
|
112
|
-
|
|
71
|
+
A host installs the gems and configures **nothing per pack**. Both halves self-wire:
|
|
113
72
|
|
|
114
|
-
|
|
73
|
+
1. **Emit routing** — at boot `event_engine` registers `DefinitionPublisher` as
|
|
74
|
+
`EventEngine::Definition.publisher`, so every pack helper routes into
|
|
75
|
+
`EventEngine.emit` automatically.
|
|
76
|
+
2. **Schema discovery** — a generated pack registers itself when required, so
|
|
77
|
+
`event_engine:catalog` finds every pack's `schema.json` with no
|
|
78
|
+
configuration. See [Building the catalog](#1-build-the-catalog).
|
|
115
79
|
|
|
116
|
-
|
|
117
|
-
|
|
80
|
+
What a host still decides for itself is **how** each event is processed, in the
|
|
81
|
+
[rules file](#declaring-how-events-are-processed).
|
|
118
82
|
|
|
119
|
-
|
|
120
|
-
# app/event_definitions/cow_fed.rb
|
|
121
|
-
class CowFed < EventEngine::EventDefinition
|
|
122
|
-
input :cow # required input to the emit helper
|
|
123
|
-
optional_input :farmer # optional input
|
|
124
|
-
|
|
125
|
-
event_name :cow_fed # the event's identity → EventEngine.cow_fed
|
|
126
|
-
event_type :domain # free-form classification (:domain, :integration, …)
|
|
127
|
-
event_level 3 # how it's dispatched (optional; see Event levels)
|
|
128
|
-
|
|
129
|
-
required_payload :weight, from: :cow, attr: :weight
|
|
130
|
-
optional_payload :farmer_name, from: :farmer, attr: :name
|
|
131
|
-
end
|
|
132
|
-
```
|
|
83
|
+
---
|
|
133
84
|
|
|
134
|
-
|
|
85
|
+
## A working example
|
|
135
86
|
|
|
136
|
-
|
|
87
|
+
[**DYB-Development/event_engine_example**](https://github.com/DYB-Development/event_engine_example)
|
|
88
|
+
is a minimal Rails app wired to this pipeline end to end — declaring an event,
|
|
89
|
+
compiling it, declaring how it is processed, and handling it. It installs all three
|
|
90
|
+
gems from RubyGems, so it is what a real host looks like.
|
|
137
91
|
|
|
138
|
-
|
|
139
|
-
|---|---|---|
|
|
140
|
-
| `event_name` | `event_name(:symbol)` | The event's identity. Becomes the `EventEngine.<name>` helper. **Required.** |
|
|
141
|
-
| `event_type` | `event_type(:symbol)` | Free-form classification, e.g. `:domain`, `:integration`, `:system`. **Required.** |
|
|
142
|
-
| `event_level` | `event_level(Integer)` | Dispatch level `1..4` (see [Event levels](#event-levels)). Optional. |
|
|
143
|
-
| `input` | `input(:name)` | Declares a **required** input keyword the emit helper accepts. Duplicate names raise `ArgumentError`. |
|
|
144
|
-
| `optional_input` | `optional_input(:name)` | Declares an **optional** input keyword. |
|
|
145
|
-
| `required_payload` | `required_payload(name, from:, attr: nil)` | A payload field that must be present. `from:` names the input it reads; `attr:` is the method called on that input. |
|
|
146
|
-
| `optional_payload` | `optional_payload(name, from:, attr: nil)` | Same, but **omitted from the payload** when the source input is `nil`. |
|
|
92
|
+
---
|
|
147
93
|
|
|
148
|
-
|
|
149
|
-
envelope/outbox columns) and rejected at dump time:
|
|
94
|
+
## Getting started
|
|
150
95
|
|
|
96
|
+
```ruby
|
|
97
|
+
# Gemfile
|
|
98
|
+
gem "event_engine"
|
|
151
99
|
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bundle install
|
|
155
103
|
```
|
|
156
104
|
|
|
157
|
-
###
|
|
105
|
+
### 1. Build the catalog
|
|
158
106
|
|
|
159
|
-
|
|
160
|
-
|
|
107
|
+
At boot the engine loads a committed **`db/event_schema.json`** and reconstructs the
|
|
108
|
+
registry from it (no Ruby is evaluated from a schema file; missing in production
|
|
109
|
+
raises). Put the schema there by either:
|
|
161
110
|
|
|
162
|
-
- `
|
|
163
|
-
-
|
|
164
|
-
itself is used (passthrough).
|
|
165
|
-
- For an `optional_payload`, if the `from:` input is `nil` the field is simply left
|
|
166
|
-
out of the payload (no key, not a `nil` value).
|
|
111
|
+
- committing `db/event_schema.json` directly, or
|
|
112
|
+
- registering a pack's slice at boot (additive — each slice merges in):
|
|
167
113
|
|
|
168
|
-
```ruby
|
|
169
|
-
|
|
170
|
-
|
|
114
|
+
```ruby
|
|
115
|
+
# in a pack's Rails engine
|
|
116
|
+
initializer "marketing_events.register_events" do
|
|
117
|
+
config.after_initialize do
|
|
118
|
+
EventEngine.register_slice!(schema_path: MarketingEvents.schema_path)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
```
|
|
171
122
|
|
|
172
|
-
|
|
173
|
-
|
|
123
|
+
To build `db/event_schema.json` from your packs, run the catalog task — it finds
|
|
124
|
+
them without configuration:
|
|
174
125
|
|
|
175
|
-
|
|
176
|
-
|
|
126
|
+
```bash
|
|
127
|
+
bin/rails event_engine:catalog
|
|
177
128
|
```
|
|
178
129
|
|
|
179
|
-
|
|
130
|
+
This writes **two** files: the catalog itself, and the
|
|
131
|
+
[rules file](#declaring-how-events-are-processed) listing every event in it.
|
|
180
132
|
|
|
181
|
-
###
|
|
133
|
+
### Where the catalog's sources come from
|
|
182
134
|
|
|
183
|
-
|
|
184
|
-
`attr:` only — there is no `type:` option and no type casting, and no
|
|
185
|
-
`entity_class` / `entity_id` / `entity_version` macros.
|
|
135
|
+
`EventEngine.schema_sources` resolves what the catalog aggregates:
|
|
186
136
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
137
|
+
1. `config.publisher_schema_paths`, when you have set it — an explicit list always wins.
|
|
138
|
+
2. Otherwise, **every pack that has registered itself**. Packs generated by
|
|
139
|
+
`event_engine-event_definition` call `EventEngine::Definition.register_pack(self)`
|
|
140
|
+
when required, so requiring a pack is enough to make its `schema.json` discoverable.
|
|
190
141
|
|
|
191
|
-
|
|
142
|
+
Discovery is optional and degrades quietly: `event_engine` does not depend on
|
|
143
|
+
`event_engine-event_definition`, so with no packs loaded there is nothing to
|
|
144
|
+
discover and `schema_sources` returns the configured list (empty by default).
|
|
192
145
|
|
|
193
|
-
|
|
194
|
-
`export_csv_completed`, `export_csv_failed` — share inputs and payload fields. Writing
|
|
195
|
-
them as three independent `EventDefinition`s lets their names and shared fields drift.
|
|
196
|
-
Subclass `EventEngine::LifecycleDefinition` to stamp the whole family from one template:
|
|
146
|
+
A host with packs in its Gemfile therefore needs no per-pack configuration:
|
|
197
147
|
|
|
198
148
|
```ruby
|
|
199
|
-
#
|
|
200
|
-
|
|
201
|
-
subject :export_csv # validated against the SubjectRegistry
|
|
202
|
-
event_type :product
|
|
203
|
-
|
|
204
|
-
input :export
|
|
205
|
-
required_payload :format, from: :export, attr: :format
|
|
206
|
-
|
|
207
|
-
lifecycle :started, :completed, :failed # → export_csv_started / _completed / _failed
|
|
208
|
-
|
|
209
|
-
on :failed do
|
|
210
|
-
input :error
|
|
211
|
-
required_payload :error_class, from: :error, attr: :class
|
|
212
|
-
end
|
|
213
|
-
end
|
|
149
|
+
EventEngine.configuration.publisher_schema_paths # => []
|
|
150
|
+
EventEngine.schema_sources # => ["/…/marketing/schema.json"]
|
|
214
151
|
```
|
|
215
152
|
|
|
216
|
-
|
|
217
|
-
each yields a working `EventEngine.export_csv_completed(...)` helper). Shared declarations
|
|
218
|
-
apply to every verb; an `on :verb` block layers additional inputs/payloads onto that verb
|
|
219
|
-
only. The generated events behave exactly like hand-written ones everywhere — schema dump,
|
|
220
|
-
registry, helpers, metadata enricher, catalog, and compatibility checks all apply unchanged.
|
|
221
|
-
|
|
222
|
-
| Macro | Signature | What it does |
|
|
223
|
-
|---|---|---|
|
|
224
|
-
| `subject` | `subject(:symbol)` | The family's subject, carried onto every generated event. Must be registered. |
|
|
225
|
-
| `event_type` | `event_type(:symbol)` | Shared across every verb. |
|
|
226
|
-
| `process_type` | `process_type(:symbol)` | Shared across every verb. Optional. |
|
|
227
|
-
| `lifecycle` | `lifecycle(*verbs)` | Generates one event per verb, named `:"#{subject}_#{verb}"`. |
|
|
228
|
-
| `on` | `on(:verb) { … }` | Layers verb-specific `input` / `required_payload` / `optional_payload` onto that verb only. Add-only. |
|
|
229
|
-
|
|
230
|
-
Shared `input` / `optional_input` / `required_payload` / `optional_payload` are declared
|
|
231
|
-
exactly as on a plain `EventDefinition` and apply to every verb.
|
|
232
|
-
|
|
233
|
-
---
|
|
234
|
-
|
|
235
|
-
## Generating the schema
|
|
236
|
-
|
|
237
|
-
After adding or changing definitions:
|
|
238
|
-
|
|
239
|
-
```bash
|
|
240
|
-
bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
|
|
241
|
-
```
|
|
153
|
+
### 2. Register your processors
|
|
242
154
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
looks like:
|
|
155
|
+
A processor is anything responding to `#call(event)`, registered under a name. Add a
|
|
156
|
+
processor gem (`event_engine-delivery`, `-store`, `-subscribers`, …) **or** your own:
|
|
246
157
|
|
|
247
158
|
```ruby
|
|
248
|
-
#
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# bin/rails event_engine:schema:dump
|
|
252
|
-
#
|
|
253
|
-
# Do not edit manually.
|
|
254
|
-
|
|
255
|
-
EventEngine::EventSchema.define do |schema|
|
|
256
|
-
schema.register(
|
|
257
|
-
EventEngine::EventDefinition::Schema.new(
|
|
258
|
-
event_name: :cow_fed,
|
|
259
|
-
event_version: 1,
|
|
260
|
-
event_type: :domain,
|
|
261
|
-
event_level: 3,
|
|
262
|
-
required_inputs: [:cow],
|
|
263
|
-
optional_inputs: [:farmer],
|
|
264
|
-
payload_fields: [
|
|
265
|
-
{ name: :weight, required: true, from: :cow, attr: :weight }
|
|
266
|
-
]
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
end
|
|
159
|
+
# config/initializers/event_engine.rb
|
|
160
|
+
EventEngine.register_processor(:subscribers, SubscriberFanout.new)
|
|
161
|
+
EventEngine.register_processor(:audit, ->(event) { AuditLog.record(event) })
|
|
270
162
|
```
|
|
271
163
|
|
|
272
|
-
###
|
|
164
|
+
### 3. Declare how each event is processed
|
|
273
165
|
|
|
274
|
-
|
|
275
|
-
|
|
166
|
+
Name the processor for each event in `config/event_rules.yml` — see
|
|
167
|
+
[the rules file](#declaring-how-events-are-processed).
|
|
276
168
|
|
|
277
|
-
|
|
278
|
-
- When you change an existing event, the merger compares a **SHA256 fingerprint**
|
|
279
|
-
of its `event_name`, `event_type`, inputs, and payload fields against the latest
|
|
280
|
-
version in the file. If they differ, it writes a **new version** (`N + 1`); if
|
|
281
|
-
they match, nothing changes.
|
|
282
|
-
- Version numbers are **monotonic** — reverting a change to a previous shape still
|
|
283
|
-
produces a *new* higher version, never reuses an old number.
|
|
169
|
+
### 4. Emit
|
|
284
170
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
> an event up the level ladder as a one-line change with no schema churn.
|
|
171
|
+
```ruby
|
|
172
|
+
EventEngine.emit(:cow_fed, inputs: { cow: cow }, domain: :sales)
|
|
173
|
+
```
|
|
289
174
|
|
|
290
|
-
|
|
175
|
+
Or, more usually, call the pack's generated helper — it routes here for you:
|
|
291
176
|
|
|
292
|
-
```
|
|
293
|
-
|
|
177
|
+
```ruby
|
|
178
|
+
MarketingEvents.lead_created(lead: lead)
|
|
294
179
|
```
|
|
295
180
|
|
|
296
|
-
This fails if your definitions have drifted from the committed `db/event_schema.rb`
|
|
297
|
-
(i.e. someone changed a definition but forgot to dump), printing a readable diff of
|
|
298
|
-
what changed. Add it to CI to keep the file honest. The older `event_engine:schema`
|
|
299
|
-
and `event_engine:schema_check` tasks perform the same check without the diff.
|
|
300
|
-
|
|
301
181
|
---
|
|
302
182
|
|
|
303
183
|
## Emitting events
|
|
304
184
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
185
|
+
`EventEngine.emit` is the always-available entry point, and the target the publisher
|
|
186
|
+
adapter forwards to. It looks the event up in the catalog, builds its payload from
|
|
187
|
+
the inputs, stamps the envelope, routes it to its processor, and returns the built
|
|
188
|
+
`EventEngine::Event`.
|
|
308
189
|
|
|
309
190
|
```ruby
|
|
310
|
-
EventEngine.
|
|
311
|
-
|
|
312
|
-
farmer: farmer,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
191
|
+
event = EventEngine.emit(
|
|
192
|
+
:cow_fed,
|
|
193
|
+
inputs: { cow: cow, farmer: farmer }, # the declared inputs, by name
|
|
194
|
+
|
|
195
|
+
domain: :sales, # scopes lookup when a name exists in >1 domain
|
|
196
|
+
event_version: 2, # optional; defaults to the latest version
|
|
197
|
+
occurred_at: Time.current, # optional; defaults to Time.current
|
|
198
|
+
metadata: { source: "import" }, # optional; merged over metadata_defaults
|
|
199
|
+
idempotency_key: "cow-#{cow.id}", # optional; defaults to a UUID
|
|
200
|
+
aggregate_type: "Cow", # optional aggregate envelope fields
|
|
318
201
|
aggregate_id: cow.id,
|
|
319
|
-
aggregate_version:
|
|
202
|
+
aggregate_version: 3
|
|
320
203
|
)
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
- Missing a required input, or passing an unknown input, raises `ArgumentError`.
|
|
324
|
-
- `event_version:` may be passed to pin a specific schema version (defaults to latest).
|
|
325
|
-
- The return value is **whatever the handlers return** — there's no canonical return
|
|
326
|
-
in core. (`event_engine-delivery`, for example, returns the persisted outbox record
|
|
327
|
-
for levels 3+.)
|
|
328
|
-
|
|
329
|
-
The built `EventEngine::Event` exposes: `event_name`, `event_type`, `event_version`,
|
|
330
|
-
`event_level`, `payload` (symbol-keyed), `metadata`, `occurred_at`,
|
|
331
|
-
`idempotency_key`, `aggregate_type`, `aggregate_id`, `aggregate_version`.
|
|
332
|
-
|
|
333
|
-
---
|
|
334
204
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
A **subscriber** reacts to an event in-process. Subclass `EventEngine::Subscriber`,
|
|
338
|
-
declare what it handles, and implement `handle`:
|
|
339
|
-
|
|
340
|
-
```ruby
|
|
341
|
-
# app/subscribers/send_welcome_email.rb
|
|
342
|
-
class SendWelcomeEmail < EventEngine::Subscriber
|
|
343
|
-
subscribes_to :user_registered
|
|
344
|
-
|
|
345
|
-
def handle(event)
|
|
346
|
-
# event.payload is symbol-keyed
|
|
347
|
-
UserMailer.welcome(event.payload[:user_id]).deliver_later
|
|
348
|
-
end
|
|
349
|
-
end
|
|
205
|
+
event.payload # => { weight: 500 } (symbol-keyed, built from inputs)
|
|
206
|
+
event.process_type # => :durable (from the rules file)
|
|
350
207
|
```
|
|
351
208
|
|
|
352
|
-
|
|
353
|
-
|
|
209
|
+
Input validation (missing required input, unknown input) raises `ArgumentError` at
|
|
210
|
+
build time.
|
|
354
211
|
|
|
355
|
-
|
|
356
|
-
> `EventEngine::SubscriberRegistry` — it does not invoke them. Invocation is done by
|
|
357
|
-
> a handler. `event_engine-delivery` invokes subscribers for levels 1–3 (see its
|
|
358
|
-
> docs). If you run core standalone, your own handler decides when/whether to call
|
|
359
|
-
> `EventEngine::SubscriberRegistry.subscribers_for(event.event_name)`.
|
|
212
|
+
### Named helpers (`MarketingEvents.lead_created`)
|
|
360
213
|
|
|
361
|
-
|
|
214
|
+
The ergonomic per-event helpers live in the **domain packs**, generated by
|
|
215
|
+
`event_engine-event_definition`. This runtime does not generate them; at boot it will
|
|
216
|
+
`load db/event_engine_helpers.rb` **if a pack has committed one**, but it never
|
|
217
|
+
creates that file. Once the [publisher adapter](#the-intended-passive-wiring-tbd)
|
|
218
|
+
is wired, calling a pack helper routes through `EventEngine.emit` for you; until then,
|
|
219
|
+
call `EventEngine.emit` directly.
|
|
362
220
|
|
|
363
221
|
---
|
|
364
222
|
|
|
365
|
-
##
|
|
223
|
+
## Declaring how events are processed
|
|
366
224
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
225
|
+
Every event in the catalog gets its processing declared in one file,
|
|
226
|
+
`config/event_rules.yml`. The catalog task writes it for you and keeps it in step
|
|
227
|
+
with the catalog.
|
|
370
228
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
229
|
+
```yaml
|
|
230
|
+
default: subscribers # anything not named below
|
|
231
|
+
packs:
|
|
232
|
+
marketing: subscribers # every event in the marketing pack
|
|
233
|
+
billing: ledger
|
|
234
|
+
events:
|
|
235
|
+
invoice_voided: audit # this one event
|
|
236
|
+
lead_converted: # catalogued but undecided
|
|
237
|
+
```
|
|
377
238
|
|
|
378
|
-
|
|
379
|
-
|
|
239
|
+
A rule names a **registered processor**. Resolution is **event → pack → default**,
|
|
240
|
+
and the resolved name is both the processor invoked and the value stamped on
|
|
241
|
+
`event.process_type`, so the two cannot disagree.
|
|
380
242
|
|
|
381
|
-
|
|
382
|
-
- Work is lost across crashes/restarts/deploys → **2 → 3**.
|
|
383
|
-
- An independent service must consume the event → **3 → 4**.
|
|
243
|
+
### Keeping it in step with the catalog
|
|
384
244
|
|
|
385
|
-
|
|
386
|
-
> core gem only stamps `event_level` onto the event and dispatches it. Levels 1–4
|
|
387
|
-
> *mean* something only once a handler that interprets them is registered. Level 5
|
|
388
|
-
> (event sourcing) is reserved but unsupported by the delivery layer.
|
|
245
|
+
`bin/rails event_engine:catalog` writes this file alongside the catalog:
|
|
389
246
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
247
|
+
- every catalogued event appears, so a new event is visible rather than silently unrouted
|
|
248
|
+
- rules you have already decided are preserved
|
|
249
|
+
- `default` and `packs` are preserved
|
|
393
250
|
|
|
394
|
-
|
|
251
|
+
A newly catalogued event appears with no value. It falls through to its pack rule or
|
|
252
|
+
the default; if neither exists, emitting it raises rather than being dropped.
|
|
395
253
|
|
|
396
|
-
|
|
254
|
+
### When a rule is wrong
|
|
397
255
|
|
|
398
|
-
|
|
399
|
-
object that responds to `call(event)`. Register it with the levels it cares about:
|
|
256
|
+
Both misconfigurations fail loudly and name the fix:
|
|
400
257
|
|
|
401
|
-
```ruby
|
|
402
|
-
EventEngine.register_handler(handler, levels: :all) # every event
|
|
403
|
-
EventEngine.register_handler(handler, levels: 1..4) # a Range
|
|
404
|
-
EventEngine.register_handler(handler, levels: [1, 3]) # an explicit list
|
|
405
258
|
```
|
|
259
|
+
EventEngine::UnroutableEventError:
|
|
260
|
+
no processing rule for event :lead_converted (pack :marketing);
|
|
261
|
+
declare it in the rules file under events, packs, or default
|
|
406
262
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
A minimal standalone handler — no companion gem required:
|
|
411
|
-
|
|
412
|
-
```ruby
|
|
413
|
-
# config/initializers/event_engine.rb
|
|
414
|
-
class LogEverythingHandler
|
|
415
|
-
def call(event)
|
|
416
|
-
Rails.logger.info("[event] #{event.event_name} v#{event.event_version} #{event.payload.inspect}")
|
|
417
|
-
event
|
|
418
|
-
end
|
|
419
|
-
end
|
|
420
|
-
|
|
421
|
-
Rails.application.config.after_initialize do
|
|
422
|
-
EventEngine.register_handler(LogEverythingHandler.new, levels: :all)
|
|
423
|
-
end
|
|
263
|
+
EventEngine::UnregisteredProcessorError:
|
|
264
|
+
the rule for event :lead_created (pack :marketing) names processor :ghost,
|
|
265
|
+
but no processor is registered under that name
|
|
424
266
|
```
|
|
425
267
|
|
|
426
|
-
|
|
268
|
+
Declaring nothing at all is still valid: with an empty rules file no processor is
|
|
269
|
+
invoked and events are built and returned as before.
|
|
427
270
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
projection dispatcher).
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Configuration
|
|
432
274
|
|
|
433
|
-
|
|
275
|
+
Set via `EventEngine.configure { |config| … }`. All fields are optional.
|
|
434
276
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
277
|
+
| Field | Default | Accepts | What it does |
|
|
278
|
+
|---|---|---|---|
|
|
279
|
+
| `schema_path` | `"db/event_schema.json"` | String / path | The committed catalog the engine loads at boot, and the file the catalog task writes. |
|
|
280
|
+
| `rules_path` | `"config/event_rules.yml"` | String / path | The processing rules the runtime loads, and the file the catalog task keeps in step. |
|
|
281
|
+
| `metadata_defaults` | `nil` | A callable (`-> { Hash }`) | Called on each emit; its hash is merged **under** any call-site `metadata:` (call-site wins). A raising callable is swallowed and logged, so emission never breaks. |
|
|
282
|
+
| `logger` | `Rails.logger` | Any Logger | Where the engine logs (missing-schema warning, a raising `metadata_defaults`). |
|
|
283
|
+
| `publisher_schema_paths` | `[]` | Array of paths | Pins the catalog task to an explicit list of `schema.json` files. Leave empty to discover registered packs. |
|
|
438
284
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
> caller. Order matters: register `event_engine-store` before/after `delivery`
|
|
442
|
-
> deliberately if both are present.
|
|
285
|
+
Processing is **not** configured here — it is declared in the
|
|
286
|
+
[rules file](#declaring-how-events-are-processed).
|
|
443
287
|
|
|
444
288
|
---
|
|
445
289
|
|
|
446
|
-
##
|
|
290
|
+
## The `Event`
|
|
447
291
|
|
|
448
|
-
|
|
292
|
+
`EventEngine::Event` is a keyword-init `Struct`:
|
|
449
293
|
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
end
|
|
294
|
+
```
|
|
295
|
+
event_name event_type event_version process_type subject domain
|
|
296
|
+
payload metadata occurred_at idempotency_key
|
|
297
|
+
aggregate_type aggregate_id aggregate_version
|
|
455
298
|
```
|
|
456
299
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
| `logger` | `Rails.logger` (or `Logger.new($stdout)` outside Rails) | Where core logs |
|
|
460
|
-
|
|
461
|
-
> Delivery options (`delivery_adapter`, `transport`, `batch_size`, …) belong to
|
|
462
|
-
> `event_engine-delivery` and are set via `EventEngine::Delivery.configure` — see that
|
|
463
|
-
> gem's README.
|
|
300
|
+
`Event.from(record)` rebuilds one from any object exposing those readers (symbolizing
|
|
301
|
+
the payload keys) — handy for processor gems reconstituting a persisted event.
|
|
464
302
|
|
|
465
303
|
---
|
|
466
304
|
|
|
467
305
|
## Rake tasks
|
|
468
306
|
|
|
469
|
-
| Task |
|
|
307
|
+
| Task | What it does |
|
|
470
308
|
|---|---|
|
|
471
|
-
| `event_engine:
|
|
472
|
-
| `event_engine:schema:verify` | Fail with a readable diff if definitions have drifted (use in CI) |
|
|
473
|
-
| `event_engine:schema` | Same drift check, no diff |
|
|
474
|
-
| `event_engine:schema_check` | Same drift check, no diff (alternate name) |
|
|
309
|
+
| `event_engine:catalog` | Builds the committed catalog at `schema_path` from every discovered pack (or from `publisher_schema_paths` when set), then keeps the rules file at `rules_path` in step with it. |
|
|
475
310
|
|
|
476
|
-
(`event_engine-delivery` adds `dead_letters:*` and `outbox:cleanup` tasks.)
|
|
477
311
|
|
|
478
312
|
---
|
|
479
313
|
|
|
480
|
-
##
|
|
314
|
+
## Known gaps
|
|
481
315
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
The core gem itself ships no migrations. If you need the outbox or the event log,
|
|
491
|
-
install the companion gem you need and run its migrations directly (see
|
|
492
|
-
`event_engine-delivery` / `event_engine-store`).
|
|
316
|
+
- **Boot requires a committed `db/event_schema.json`.** The engine raises in
|
|
317
|
+
production if it is missing. Building it is one task (`event_engine:catalog`),
|
|
318
|
+
but it does have to be committed.
|
|
319
|
+
- **`the_local` guides.** The AI-assistant reference under
|
|
320
|
+
`lib/event_engine/reference/` and the `the_local` subagents still describe the old
|
|
321
|
+
in-gem DSL. They are generated provider docs (owned by `the_local-develop`) and
|
|
322
|
+
will be regenerated to match this runtime.
|
|
493
323
|
|
|
494
324
|
---
|
|
495
325
|
|
|
496
|
-
##
|
|
497
|
-
|
|
498
|
-
A condensed, authoritative API reference ships inside the gem at
|
|
499
|
-
`lib/event_engine/reference/guide.md` and is installed into consuming apps as Claude
|
|
500
|
-
Code subagents (`.claude/agents/`). When working in a host app, prefer that
|
|
501
|
-
reference and this README over reading gem internals.
|
|
326
|
+
## Development
|
|
502
327
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
1. Fork and create a feature branch.
|
|
508
|
-
2. Add tests for behavior changes (Minitest; see `test/`).
|
|
509
|
-
3. Run the suite: `bundle exec rake test`.
|
|
510
|
-
4. Open a PR.
|
|
328
|
+
```bash
|
|
329
|
+
bundle install
|
|
330
|
+
bundle exec rake test # Minitest, via the dummy app in test/dummy
|
|
331
|
+
```
|
|
511
332
|
|
|
512
333
|
---
|
|
513
334
|
|
|
514
335
|
## License
|
|
515
336
|
|
|
516
|
-
Available as open source under the terms of the
|
|
517
|
-
[MIT License](https://opensource.org/licenses/MIT).
|
|
337
|
+
Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|