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
data/README.md
CHANGED
|
@@ -1,517 +1,328 @@
|
|
|
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
|
-
|
|
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.
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
[The handler extension point](#the-handler-extension-point).
|
|
22
|
+
---
|
|
22
23
|
|
|
23
|
-
|
|
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`.
|
|
24
|
+
## Where this gem sits
|
|
26
25
|
|
|
27
|
-
|
|
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 |
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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)
|
|
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.
|
|
51
37
|
|
|
52
38
|
---
|
|
53
39
|
|
|
54
|
-
##
|
|
40
|
+
## How it fits together
|
|
55
41
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
```
|
|
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.
|
|
60
45
|
|
|
61
|
-
```
|
|
62
|
-
|
|
46
|
+
```
|
|
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)
|
|
63
62
|
```
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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.
|
|
82
|
-
|
|
83
|
-
---
|
|
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.
|
|
84
68
|
|
|
85
|
-
|
|
69
|
+
### The passive wiring
|
|
86
70
|
|
|
87
|
-
|
|
88
|
-
EventDefinition (Ruby DSL)
|
|
89
|
-
│ bin/rails event_engine:schema:dump
|
|
90
|
-
▼
|
|
91
|
-
db/event_schema.rb ◄── authoritative at runtime; commit it
|
|
92
|
-
│ Rails boot (Engine initializer)
|
|
93
|
-
▼
|
|
94
|
-
SchemaRegistry ──► installs EventEngine.<event_name> helpers
|
|
95
|
-
│ you call EventEngine.cow_fed(cow: cow)
|
|
96
|
-
▼
|
|
97
|
-
EventBuilder builds a validated EventEngine::Event
|
|
98
|
-
│ EventEngine.dispatch(event)
|
|
99
|
-
▼
|
|
100
|
-
HandlerRegistry ──► every registered handler whose `levels:` match event_level
|
|
101
|
-
(event_engine-delivery, event_engine-store, or your own)
|
|
102
|
-
```
|
|
71
|
+
A host installs the gems and configures **nothing per pack**. Both halves self-wire:
|
|
103
72
|
|
|
104
|
-
|
|
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).
|
|
105
79
|
|
|
106
|
-
|
|
107
|
-
|
|
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.
|
|
80
|
+
What a host still decides for itself is **how** each event is processed, in the
|
|
81
|
+
[rules file](#declaring-how-events-are-processed).
|
|
111
82
|
|
|
112
83
|
---
|
|
113
84
|
|
|
114
|
-
##
|
|
115
|
-
|
|
116
|
-
Put definitions where Rails eager-loads them — conventionally
|
|
117
|
-
`app/event_definitions/`. Subclass `EventEngine::EventDefinition`:
|
|
85
|
+
## Getting started
|
|
118
86
|
|
|
119
87
|
```ruby
|
|
120
|
-
#
|
|
121
|
-
|
|
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
|
|
88
|
+
# Gemfile
|
|
89
|
+
gem "event_engine"
|
|
132
90
|
```
|
|
133
91
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
All methods below are **class-level** macros on an `EventDefinition` subclass.
|
|
137
|
-
|
|
138
|
-
| Macro | Signature | What it does |
|
|
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`. |
|
|
147
|
-
|
|
148
|
-
A handful of payload field names are **reserved** (they collide with event
|
|
149
|
-
envelope/outbox columns) and rejected at dump time:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
event_name event_type event_version occurred_at created_at updated_at
|
|
153
|
-
published_at metadata idempotency_key attempts dead_lettered_at
|
|
154
|
-
aggregate_type aggregate_id aggregate_version
|
|
92
|
+
```bash
|
|
93
|
+
bundle install
|
|
155
94
|
```
|
|
156
95
|
|
|
157
|
-
###
|
|
96
|
+
### 1. Build the catalog
|
|
158
97
|
|
|
159
|
-
|
|
160
|
-
|
|
98
|
+
At boot the engine loads a committed **`db/event_schema.json`** and reconstructs the
|
|
99
|
+
registry from it (no Ruby is evaluated from a schema file; missing in production
|
|
100
|
+
raises). Put the schema there by either:
|
|
161
101
|
|
|
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).
|
|
102
|
+
- committing `db/event_schema.json` directly, or
|
|
103
|
+
- registering a pack's slice at boot (additive — each slice merges in):
|
|
167
104
|
|
|
168
|
-
```ruby
|
|
169
|
-
|
|
170
|
-
|
|
105
|
+
```ruby
|
|
106
|
+
# in a pack's Rails engine
|
|
107
|
+
initializer "marketing_events.register_events" do
|
|
108
|
+
config.after_initialize do
|
|
109
|
+
EventEngine.register_slice!(schema_path: MarketingEvents.schema_path)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
```
|
|
171
113
|
|
|
172
|
-
|
|
173
|
-
|
|
114
|
+
To build `db/event_schema.json` from your packs, run the catalog task — it finds
|
|
115
|
+
them without configuration:
|
|
174
116
|
|
|
175
|
-
|
|
176
|
-
|
|
117
|
+
```bash
|
|
118
|
+
bin/rails event_engine:catalog
|
|
177
119
|
```
|
|
178
120
|
|
|
179
|
-
|
|
121
|
+
This writes **two** files: the catalog itself, and the
|
|
122
|
+
[rules file](#declaring-how-events-are-processed) listing every event in it.
|
|
180
123
|
|
|
181
|
-
###
|
|
124
|
+
### Where the catalog's sources come from
|
|
182
125
|
|
|
183
|
-
|
|
184
|
-
`attr:` only — there is no `type:` option and no type casting, and no
|
|
185
|
-
`entity_class` / `entity_id` / `entity_version` macros.
|
|
126
|
+
`EventEngine.schema_sources` resolves what the catalog aggregates:
|
|
186
127
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
`
|
|
128
|
+
1. `config.publisher_schema_paths`, when you have set it — an explicit list always wins.
|
|
129
|
+
2. Otherwise, **every pack that has registered itself**. Packs generated by
|
|
130
|
+
`event_engine-event_definition` call `EventEngine::Definition.register_pack(self)`
|
|
131
|
+
when required, so requiring a pack is enough to make its `schema.json` discoverable.
|
|
190
132
|
|
|
191
|
-
|
|
133
|
+
Discovery is optional and degrades quietly: `event_engine` does not depend on
|
|
134
|
+
`event_engine-event_definition`, so with no packs loaded there is nothing to
|
|
135
|
+
discover and `schema_sources` returns the configured list (empty by default).
|
|
192
136
|
|
|
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:
|
|
137
|
+
A host with packs in its Gemfile therefore needs no per-pack configuration:
|
|
197
138
|
|
|
198
139
|
```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
|
|
140
|
+
EventEngine.configuration.publisher_schema_paths # => []
|
|
141
|
+
EventEngine.schema_sources # => ["/…/marketing/schema.json"]
|
|
214
142
|
```
|
|
215
143
|
|
|
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
|
-
```
|
|
144
|
+
### 2. Register your processors
|
|
242
145
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
looks like:
|
|
146
|
+
A processor is anything responding to `#call(event)`, registered under a name. Add a
|
|
147
|
+
processor gem (`event_engine-delivery`, `-store`, `-subscribers`, …) **or** your own:
|
|
246
148
|
|
|
247
149
|
```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
|
|
150
|
+
# config/initializers/event_engine.rb
|
|
151
|
+
EventEngine.register_processor(:subscribers, SubscriberFanout.new)
|
|
152
|
+
EventEngine.register_processor(:audit, ->(event) { AuditLog.record(event) })
|
|
270
153
|
```
|
|
271
154
|
|
|
272
|
-
###
|
|
155
|
+
### 3. Declare how each event is processed
|
|
273
156
|
|
|
274
|
-
|
|
275
|
-
|
|
157
|
+
Name the processor for each event in `config/event_rules.yml` — see
|
|
158
|
+
[the rules file](#declaring-how-events-are-processed).
|
|
276
159
|
|
|
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.
|
|
160
|
+
### 4. Emit
|
|
284
161
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
> an event up the level ladder as a one-line change with no schema churn.
|
|
162
|
+
```ruby
|
|
163
|
+
EventEngine.emit(:cow_fed, inputs: { cow: cow }, domain: :sales)
|
|
164
|
+
```
|
|
289
165
|
|
|
290
|
-
|
|
166
|
+
Or, more usually, call the pack's generated helper — it routes here for you:
|
|
291
167
|
|
|
292
|
-
```
|
|
293
|
-
|
|
168
|
+
```ruby
|
|
169
|
+
MarketingEvents.lead_created(lead: lead)
|
|
294
170
|
```
|
|
295
171
|
|
|
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
172
|
---
|
|
302
173
|
|
|
303
174
|
## Emitting events
|
|
304
175
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
176
|
+
`EventEngine.emit` is the always-available entry point, and the target the publisher
|
|
177
|
+
adapter forwards to. It looks the event up in the catalog, builds its payload from
|
|
178
|
+
the inputs, stamps the envelope, routes it to its processor, and returns the built
|
|
179
|
+
`EventEngine::Event`.
|
|
308
180
|
|
|
309
181
|
```ruby
|
|
310
|
-
EventEngine.
|
|
311
|
-
|
|
312
|
-
farmer: farmer,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
182
|
+
event = EventEngine.emit(
|
|
183
|
+
:cow_fed,
|
|
184
|
+
inputs: { cow: cow, farmer: farmer }, # the declared inputs, by name
|
|
185
|
+
|
|
186
|
+
domain: :sales, # scopes lookup when a name exists in >1 domain
|
|
187
|
+
event_version: 2, # optional; defaults to the latest version
|
|
188
|
+
occurred_at: Time.current, # optional; defaults to Time.current
|
|
189
|
+
metadata: { source: "import" }, # optional; merged over metadata_defaults
|
|
190
|
+
idempotency_key: "cow-#{cow.id}", # optional; defaults to a UUID
|
|
191
|
+
aggregate_type: "Cow", # optional aggregate envelope fields
|
|
318
192
|
aggregate_id: cow.id,
|
|
319
|
-
aggregate_version:
|
|
193
|
+
aggregate_version: 3
|
|
320
194
|
)
|
|
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
|
-
|
|
335
|
-
## Subscribers
|
|
336
195
|
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
196
|
+
event.payload # => { weight: 500 } (symbol-keyed, built from inputs)
|
|
197
|
+
event.process_type # => :durable (from the rules file)
|
|
350
198
|
```
|
|
351
199
|
|
|
352
|
-
|
|
353
|
-
|
|
200
|
+
Input validation (missing required input, unknown input) raises `ArgumentError` at
|
|
201
|
+
build time.
|
|
354
202
|
|
|
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)`.
|
|
203
|
+
### Named helpers (`MarketingEvents.lead_created`)
|
|
360
204
|
|
|
361
|
-
|
|
205
|
+
The ergonomic per-event helpers live in the **domain packs**, generated by
|
|
206
|
+
`event_engine-event_definition`. This runtime does not generate them; at boot it will
|
|
207
|
+
`load db/event_engine_helpers.rb` **if a pack has committed one**, but it never
|
|
208
|
+
creates that file. Once the [publisher adapter](#the-intended-passive-wiring-tbd)
|
|
209
|
+
is wired, calling a pack helper routes through `EventEngine.emit` for you; until then,
|
|
210
|
+
call `EventEngine.emit` directly.
|
|
362
211
|
|
|
363
212
|
---
|
|
364
213
|
|
|
365
|
-
##
|
|
214
|
+
## Declaring how events are processed
|
|
366
215
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
216
|
+
Every event in the catalog gets its processing declared in one file,
|
|
217
|
+
`config/event_rules.yml`. The catalog task writes it for you and keeps it in step
|
|
218
|
+
with the catalog.
|
|
370
219
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
220
|
+
```yaml
|
|
221
|
+
default: subscribers # anything not named below
|
|
222
|
+
packs:
|
|
223
|
+
marketing: subscribers # every event in the marketing pack
|
|
224
|
+
billing: ledger
|
|
225
|
+
events:
|
|
226
|
+
invoice_voided: audit # this one event
|
|
227
|
+
lead_converted: # catalogued but undecided
|
|
228
|
+
```
|
|
377
229
|
|
|
378
|
-
|
|
379
|
-
|
|
230
|
+
A rule names a **registered processor**. Resolution is **event → pack → default**,
|
|
231
|
+
and the resolved name is both the processor invoked and the value stamped on
|
|
232
|
+
`event.process_type`, so the two cannot disagree.
|
|
380
233
|
|
|
381
|
-
|
|
382
|
-
- Work is lost across crashes/restarts/deploys → **2 → 3**.
|
|
383
|
-
- An independent service must consume the event → **3 → 4**.
|
|
234
|
+
### Keeping it in step with the catalog
|
|
384
235
|
|
|
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.
|
|
236
|
+
`bin/rails event_engine:catalog` writes this file alongside the catalog:
|
|
389
237
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
238
|
+
- every catalogued event appears, so a new event is visible rather than silently unrouted
|
|
239
|
+
- rules you have already decided are preserved
|
|
240
|
+
- `default` and `packs` are preserved
|
|
393
241
|
|
|
394
|
-
|
|
242
|
+
A newly catalogued event appears with no value. It falls through to its pack rule or
|
|
243
|
+
the default; if neither exists, emitting it raises rather than being dropped.
|
|
395
244
|
|
|
396
|
-
|
|
245
|
+
### When a rule is wrong
|
|
397
246
|
|
|
398
|
-
|
|
399
|
-
object that responds to `call(event)`. Register it with the levels it cares about:
|
|
247
|
+
Both misconfigurations fail loudly and name the fix:
|
|
400
248
|
|
|
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
249
|
```
|
|
250
|
+
EventEngine::UnroutableEventError:
|
|
251
|
+
no processing rule for event :lead_converted (pack :marketing);
|
|
252
|
+
declare it in the rules file under events, packs, or default
|
|
406
253
|
|
|
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
|
|
254
|
+
EventEngine::UnregisteredProcessorError:
|
|
255
|
+
the rule for event :lead_created (pack :marketing) names processor :ghost,
|
|
256
|
+
but no processor is registered under that name
|
|
424
257
|
```
|
|
425
258
|
|
|
426
|
-
|
|
259
|
+
Declaring nothing at all is still valid: with an empty rules file no processor is
|
|
260
|
+
invoked and events are built and returned as before.
|
|
261
|
+
|
|
262
|
+
---
|
|
427
263
|
|
|
428
|
-
|
|
429
|
-
level (sync subscribers / background job / outbox / broker).
|
|
430
|
-
- **`event_engine-store`** registers two handlers at `levels: :all` (a recorder and a
|
|
431
|
-
projection dispatcher).
|
|
264
|
+
## Configuration
|
|
432
265
|
|
|
433
|
-
|
|
266
|
+
Set via `EventEngine.configure { |config| … }`. All fields are optional.
|
|
434
267
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
268
|
+
| Field | Default | Accepts | What it does |
|
|
269
|
+
|---|---|---|---|
|
|
270
|
+
| `schema_path` | `"db/event_schema.json"` | String / path | The committed catalog the engine loads at boot, and the file the catalog task writes. |
|
|
271
|
+
| `rules_path` | `"config/event_rules.yml"` | String / path | The processing rules the runtime loads, and the file the catalog task keeps in step. |
|
|
272
|
+
| `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. |
|
|
273
|
+
| `logger` | `Rails.logger` | Any Logger | Where the engine logs (missing-schema warning, a raising `metadata_defaults`). |
|
|
274
|
+
| `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
275
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
> caller. Order matters: register `event_engine-store` before/after `delivery`
|
|
442
|
-
> deliberately if both are present.
|
|
276
|
+
Processing is **not** configured here — it is declared in the
|
|
277
|
+
[rules file](#declaring-how-events-are-processed).
|
|
443
278
|
|
|
444
279
|
---
|
|
445
280
|
|
|
446
|
-
##
|
|
281
|
+
## The `Event`
|
|
447
282
|
|
|
448
|
-
|
|
283
|
+
`EventEngine::Event` is a keyword-init `Struct`:
|
|
449
284
|
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
end
|
|
285
|
+
```
|
|
286
|
+
event_name event_type event_version process_type subject domain
|
|
287
|
+
payload metadata occurred_at idempotency_key
|
|
288
|
+
aggregate_type aggregate_id aggregate_version
|
|
455
289
|
```
|
|
456
290
|
|
|
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.
|
|
291
|
+
`Event.from(record)` rebuilds one from any object exposing those readers (symbolizing
|
|
292
|
+
the payload keys) — handy for processor gems reconstituting a persisted event.
|
|
464
293
|
|
|
465
294
|
---
|
|
466
295
|
|
|
467
296
|
## Rake tasks
|
|
468
297
|
|
|
469
|
-
| Task |
|
|
298
|
+
| Task | What it does |
|
|
470
299
|
|---|---|
|
|
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) |
|
|
300
|
+
| `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
301
|
|
|
476
|
-
(`event_engine-delivery` adds `dead_letters:*` and `outbox:cleanup` tasks.)
|
|
477
302
|
|
|
478
303
|
---
|
|
479
304
|
|
|
480
|
-
##
|
|
305
|
+
## Known gaps
|
|
481
306
|
|
|
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`).
|
|
307
|
+
- **Boot requires a committed `db/event_schema.json`.** The engine raises in
|
|
308
|
+
production if it is missing. Building it is one task (`event_engine:catalog`),
|
|
309
|
+
but it does have to be committed.
|
|
310
|
+
- **`the_local` guides.** The AI-assistant reference under
|
|
311
|
+
`lib/event_engine/reference/` and the `the_local` subagents still describe the old
|
|
312
|
+
in-gem DSL. They are generated provider docs (owned by `the_local-develop`) and
|
|
313
|
+
will be regenerated to match this runtime.
|
|
493
314
|
|
|
494
315
|
---
|
|
495
316
|
|
|
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.
|
|
317
|
+
## Development
|
|
502
318
|
|
|
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.
|
|
319
|
+
```bash
|
|
320
|
+
bundle install
|
|
321
|
+
bundle exec rake test # Minitest, via the dummy app in test/dummy
|
|
322
|
+
```
|
|
511
323
|
|
|
512
324
|
---
|
|
513
325
|
|
|
514
326
|
## License
|
|
515
327
|
|
|
516
|
-
Available as open source under the terms of the
|
|
517
|
-
[MIT License](https://opensource.org/licenses/MIT).
|
|
328
|
+
Available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|