event_engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +53 -0
- data/MIT-LICENSE +20 -0
- data/README.md +517 -0
- data/Rakefile +12 -0
- data/app/assets/config/event_engine_manifest.js +1 -0
- data/app/assets/stylesheets/event_engine/application.css +15 -0
- data/app/controllers/event_engine/application_controller.rb +4 -0
- data/app/helpers/event_engine/application_helper.rb +4 -0
- data/app/jobs/event_engine/application_job.rb +4 -0
- data/app/mailers/event_engine/application_mailer.rb +6 -0
- data/app/models/event_engine/application_record.rb +5 -0
- data/app/views/layouts/event_engine/application.html.erb +15 -0
- data/config/routes.rb +2 -0
- data/lib/event_engine/configuration.rb +20 -0
- data/lib/event_engine/definition_loader.rb +26 -0
- data/lib/event_engine/dsl_compiler.rb +50 -0
- data/lib/event_engine/engine.rb +56 -0
- data/lib/event_engine/event.rb +32 -0
- data/lib/event_engine/event_builder.rb +45 -0
- data/lib/event_engine/event_definition/inputs.rb +43 -0
- data/lib/event_engine/event_definition/payloads.rb +47 -0
- data/lib/event_engine/event_definition/schemas.rb +158 -0
- data/lib/event_engine/event_definition/validation.rb +18 -0
- data/lib/event_engine/event_definition.rb +76 -0
- data/lib/event_engine/event_schema.rb +99 -0
- data/lib/event_engine/event_schema_dumper.rb +13 -0
- data/lib/event_engine/event_schema_loader.rb +37 -0
- data/lib/event_engine/event_schema_merger.rb +62 -0
- data/lib/event_engine/event_schema_writer.rb +47 -0
- data/lib/event_engine/handler_registry.rb +23 -0
- data/lib/event_engine/lifecycle_definition.rb +86 -0
- data/lib/event_engine/process_type.rb +26 -0
- data/lib/event_engine/railtie.rb +9 -0
- data/lib/event_engine/reference/guide.md +129 -0
- data/lib/event_engine/reference.rb +16 -0
- data/lib/event_engine/schema_catalog.rb +50 -0
- data/lib/event_engine/schema_compatibility.rb +50 -0
- data/lib/event_engine/schema_diff.rb +35 -0
- data/lib/event_engine/schema_drift_guard.rb +38 -0
- data/lib/event_engine/schema_registry.rb +122 -0
- data/lib/event_engine/subject_registry.rb +40 -0
- data/lib/event_engine/the_local/agents/event_engine-develop.md +142 -0
- data/lib/event_engine/the_local/agents/event_engine-info.md +140 -0
- data/lib/event_engine/the_local/agents/event_engine-install.md +140 -0
- data/lib/event_engine/the_local.rb +55 -0
- data/lib/event_engine/version.rb +3 -0
- data/lib/event_engine.rb +197 -0
- data/lib/generators/event_engine/install_generator.rb +31 -0
- data/lib/generators/event_engine/templates/event_schema.rb +10 -0
- data/lib/generators/event_engine/templates/initializer.rb +4 -0
- data/lib/tasks/event_engine_catalog.rake +13 -0
- data/lib/tasks/event_engine_schema.rake +82 -0
- data/lib/tasks/event_engine_schema_check.rake +20 -0
- data/lib/tasks/event_engine_tasks.rake +4 -0
- metadata +127 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
# In-memory registry holding all event schemas by name and version.
|
|
3
|
+
# Loaded once at boot from the compiled schema file.
|
|
4
|
+
#
|
|
5
|
+
# @example Look up a schema
|
|
6
|
+
# schema = registry.schema(:cow_fed)
|
|
7
|
+
# schema.event_version #=> 2
|
|
8
|
+
#
|
|
9
|
+
# @example Look up a specific version
|
|
10
|
+
# schema = registry.schema(:cow_fed, version: 1)
|
|
11
|
+
class SchemaRegistry
|
|
12
|
+
# Raised when looking up an event name or version that doesn't exist.
|
|
13
|
+
class UnknownEventError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Raised when the registry is accessed before loading or loaded twice.
|
|
16
|
+
class RegistryFrozenError < StandardError; end
|
|
17
|
+
|
|
18
|
+
# @param event_schema [EventSchema] initial schema (empty by default)
|
|
19
|
+
def initialize(event_schema = EventSchema.new)
|
|
20
|
+
@event_schema = event_schema
|
|
21
|
+
@loaded = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Registers a schema into the underlying EventSchema store.
|
|
25
|
+
#
|
|
26
|
+
# @param schema [EventDefinition::Schema] the schema to register
|
|
27
|
+
def register(schema)
|
|
28
|
+
@event_schema.register(schema)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns all registered event names.
|
|
32
|
+
#
|
|
33
|
+
# @return [Array<Symbol>]
|
|
34
|
+
def events
|
|
35
|
+
@event_schema.events
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns all version numbers for a given event.
|
|
39
|
+
#
|
|
40
|
+
# @param event_name [Symbol]
|
|
41
|
+
# @return [Array<Integer>] sorted version numbers
|
|
42
|
+
def versions_for(event_name)
|
|
43
|
+
@event_schema.versions_for(event_name)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Populates the registry from a loaded EventSchema. Can only be called once.
|
|
47
|
+
#
|
|
48
|
+
# @param schema [EventSchema] the compiled schema
|
|
49
|
+
# @return [self]
|
|
50
|
+
# @raise [RegistryFrozenError] if already loaded
|
|
51
|
+
def load_from_schema!(schema)
|
|
52
|
+
raise RegistryFrozenError, "EventRegistry already loaded" if loaded?
|
|
53
|
+
@event_schema = schema
|
|
54
|
+
|
|
55
|
+
@loaded = true
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Clears the registry, allowing it to be reloaded.
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
62
|
+
def reset!
|
|
63
|
+
@event_schema = {}
|
|
64
|
+
@loaded = false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Looks up a schema by event name and optional version.
|
|
68
|
+
# Returns the latest version when no version is specified.
|
|
69
|
+
#
|
|
70
|
+
# @param event_name [Symbol]
|
|
71
|
+
# @param version [Integer, nil] specific version, or nil for latest
|
|
72
|
+
# @return [EventDefinition::Schema]
|
|
73
|
+
# @raise [RegistryFrozenError] if registry is not loaded
|
|
74
|
+
# @raise [UnknownEventError] if event or version is not found
|
|
75
|
+
def schema(event_name, version: nil)
|
|
76
|
+
raise RegistryFrozenError, "EventRegistry not loaded" unless loaded?
|
|
77
|
+
|
|
78
|
+
schema =
|
|
79
|
+
if version
|
|
80
|
+
@event_schema.schema_for(event_name, version)
|
|
81
|
+
else
|
|
82
|
+
@event_schema.latest_for(event_name)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
unless schema
|
|
86
|
+
raise UnknownEventError,
|
|
87
|
+
"Unknown #{version ? "version #{version} for " : ""}event: #{event_name}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
schema
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the latest schema version for an event.
|
|
94
|
+
#
|
|
95
|
+
# @param event_name [Symbol]
|
|
96
|
+
# @return [EventDefinition::Schema, nil]
|
|
97
|
+
def latest_for(event_name)
|
|
98
|
+
@event_schema.latest_for(event_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Returns the underlying EventSchema store.
|
|
102
|
+
#
|
|
103
|
+
# @return [EventSchema]
|
|
104
|
+
def event_schema
|
|
105
|
+
@event_schema
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Freezes the underlying schema, preventing further modifications.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def finalize!
|
|
112
|
+
@event_schema.finalize!
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Whether the registry has been loaded with schemas.
|
|
116
|
+
#
|
|
117
|
+
# @return [Boolean]
|
|
118
|
+
def loaded?
|
|
119
|
+
@loaded == true
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module EventEngine
|
|
2
|
+
class SubjectRegistry
|
|
3
|
+
class UnknownSubjectError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class Subject
|
|
6
|
+
attr_reader :name, :metadata
|
|
7
|
+
|
|
8
|
+
def initialize(name, **metadata)
|
|
9
|
+
@name = name
|
|
10
|
+
@metadata = metadata
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.define(&block)
|
|
15
|
+
registry = new
|
|
16
|
+
registry.instance_eval(&block) if block
|
|
17
|
+
registry
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize
|
|
21
|
+
@subjects = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def subject(name, **metadata)
|
|
25
|
+
@subjects[name] = Subject.new(name, **metadata)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def [](name)
|
|
29
|
+
@subjects[name]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def registered?(name)
|
|
33
|
+
@subjects.key?(name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def names
|
|
37
|
+
@subjects.keys
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-develop
|
|
3
|
+
description: Use PROACTIVELY for any EventEngine work — defining events, choosing process_type, emitting, and keeping the committed schema in sync. MUST BE USED instead of hand-writing event plumbing.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You build EventEngine events following the reference's conventions: one
|
|
8
|
+
EventDefinition class per event in app/event_definitions/, payloads composed from
|
|
9
|
+
inputs, process_type set explicitly, emitted through the generated
|
|
10
|
+
EventEngine.<event_name> helpers. After any definition change you run
|
|
11
|
+
`bin/rails event_engine:schema:dump` and commit db/event_schema.rb, keeping
|
|
12
|
+
event_engine:schema_check green. You keep handlers idempotent.
|
|
13
|
+
|
|
14
|
+
## EventEngine
|
|
15
|
+
|
|
16
|
+
> **DO NOT** explore the event_engine gem source code. This reference is the
|
|
17
|
+
> complete user-facing API, embedded verbatim into every event_engine local so
|
|
18
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
19
|
+
|
|
20
|
+
EventEngine is a Rails engine for defining domain events as declarative classes,
|
|
21
|
+
compiling them to a committed schema, emitting them through generated helpers, and
|
|
22
|
+
dispatching them to registered handlers. Core builds and routes events; it ships no
|
|
23
|
+
handlers of its own. Durable delivery, an event store, and ready-made subscriber
|
|
24
|
+
classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
|
|
25
|
+
`event_engine-subscribers`) — this reference covers core only.
|
|
26
|
+
|
|
27
|
+
### What it offers
|
|
28
|
+
|
|
29
|
+
**Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
class CowFed < EventEngine::EventDefinition
|
|
33
|
+
event_name :cow_fed # the event's identity (required)
|
|
34
|
+
event_type :domain # classification, e.g. :domain (required)
|
|
35
|
+
process_type :durable # routing type (optional; set it explicitly)
|
|
36
|
+
|
|
37
|
+
input :cow # a required input
|
|
38
|
+
optional_input :farmer # an optional input
|
|
39
|
+
|
|
40
|
+
required_payload :weight, from: :cow, attr: :weight
|
|
41
|
+
optional_payload :farmer_name, from: :farmer, attr: :name
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
| DSL method | Purpose |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
|
|
48
|
+
| `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
|
|
49
|
+
| `process_type(:symbol)` | Routing type (optional). One of the six values below. |
|
|
50
|
+
| `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
|
|
51
|
+
| `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
|
|
52
|
+
| `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
|
|
53
|
+
|
|
54
|
+
Duplicate input names raise `ArgumentError`; payload `from:` must reference a
|
|
55
|
+
declared input.
|
|
56
|
+
|
|
57
|
+
**process_type** — core stamps this symbol onto every emitted event but does not act
|
|
58
|
+
on it. Which handlers receive an event is decided by each handler's `levels:`. The
|
|
59
|
+
values:
|
|
60
|
+
|
|
61
|
+
| value | intent |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `:inline` | handled in-process, synchronously |
|
|
64
|
+
| `:background` | handled in-process, via a background job |
|
|
65
|
+
| `:durable` | handled when a durable outbox drains |
|
|
66
|
+
| `:broker` | published to an external transport |
|
|
67
|
+
| `:telemetry` | metrics / observability handlers |
|
|
68
|
+
| `:sourced` | an append-only event store |
|
|
69
|
+
|
|
70
|
+
The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
|
|
71
|
+
etc. their behavior; core just routes to whatever is registered. If `process_type`
|
|
72
|
+
is omitted it is `nil` — set it explicitly so routing intent is clear.
|
|
73
|
+
|
|
74
|
+
**Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
EventEngine.cow_fed(
|
|
78
|
+
cow: cow, farmer: farmer, # declared inputs, by name
|
|
79
|
+
occurred_at: Time.current, # optional, defaults to now
|
|
80
|
+
metadata: { request_id: "abc" }, # optional
|
|
81
|
+
idempotency_key: "…", # optional, defaults to a UUID
|
|
82
|
+
aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
|
|
83
|
+
event_version: 1 # optional, defaults to the latest schema version
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Missing a required input, or passing an unknown one, raises `ArgumentError`. The
|
|
88
|
+
event's `payload` is symbol-keyed.
|
|
89
|
+
|
|
90
|
+
**Register handlers** — a handler is any object responding to `call(event)`:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
|
|
94
|
+
EventEngine.dispatch(event) # fan an event out (emit helpers call this)
|
|
95
|
+
EventEngine.reset_handlers! # clear all handlers
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Handlers run synchronously in registration order; if one raises, the rest don't run.
|
|
99
|
+
Keep handlers idempotent.
|
|
100
|
+
|
|
101
|
+
**Configure** — `config/initializers/event_engine.rb`, logger only:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
EventEngine.configure { |config| config.logger = Rails.logger }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
|
|
108
|
+
is authoritative at boot:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
|
|
112
|
+
bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
A new event is version 1; changing an event's identity or payload bumps its version.
|
|
116
|
+
Changing only `process_type` does not bump the version.
|
|
117
|
+
|
|
118
|
+
### Install
|
|
119
|
+
|
|
120
|
+
1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
|
|
121
|
+
2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
|
|
122
|
+
`config/initializers/event_engine.rb`.
|
|
123
|
+
3. Define events as classes in `app/event_definitions/`.
|
|
124
|
+
4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
|
|
125
|
+
5. Set `config.logger` in the initializer if you want something other than the default.
|
|
126
|
+
|
|
127
|
+
Durable delivery, an event store, and prebuilt subscriber classes are separate gems
|
|
128
|
+
(`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
|
|
129
|
+
when you need them and follow their own setup.
|
|
130
|
+
|
|
131
|
+
### EventEngine conventions
|
|
132
|
+
|
|
133
|
+
- Define one `EventDefinition` class per event in `app/event_definitions/`; never
|
|
134
|
+
hand-build event hashes.
|
|
135
|
+
- Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
|
|
136
|
+
raw payload hashes to the emit helper.
|
|
137
|
+
- Always set `process_type` explicitly so routing intent is clear.
|
|
138
|
+
- Emit only through the generated `EventEngine.<event_name>` helpers, passing the
|
|
139
|
+
declared inputs.
|
|
140
|
+
- Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
|
|
141
|
+
definition change; keep `event_engine:schema_check` green in CI.
|
|
142
|
+
- Keep handlers and subscribers idempotent.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-info
|
|
3
|
+
description: Use to learn what EventEngine offers — the event-definition DSL, process_type, emitting, handlers, and the schema workflow.
|
|
4
|
+
tools: Read
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You explain how EventEngine works and how to use it, answering only from the
|
|
8
|
+
reference: defining events, process_type routing, emitting through the generated
|
|
9
|
+
helpers, registering handlers, and the schema dump/check workflow. You make no
|
|
10
|
+
changes.
|
|
11
|
+
|
|
12
|
+
## EventEngine
|
|
13
|
+
|
|
14
|
+
> **DO NOT** explore the event_engine gem source code. This reference is the
|
|
15
|
+
> complete user-facing API, embedded verbatim into every event_engine local so
|
|
16
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
17
|
+
|
|
18
|
+
EventEngine is a Rails engine for defining domain events as declarative classes,
|
|
19
|
+
compiling them to a committed schema, emitting them through generated helpers, and
|
|
20
|
+
dispatching them to registered handlers. Core builds and routes events; it ships no
|
|
21
|
+
handlers of its own. Durable delivery, an event store, and ready-made subscriber
|
|
22
|
+
classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
|
|
23
|
+
`event_engine-subscribers`) — this reference covers core only.
|
|
24
|
+
|
|
25
|
+
### What it offers
|
|
26
|
+
|
|
27
|
+
**Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class CowFed < EventEngine::EventDefinition
|
|
31
|
+
event_name :cow_fed # the event's identity (required)
|
|
32
|
+
event_type :domain # classification, e.g. :domain (required)
|
|
33
|
+
process_type :durable # routing type (optional; set it explicitly)
|
|
34
|
+
|
|
35
|
+
input :cow # a required input
|
|
36
|
+
optional_input :farmer # an optional input
|
|
37
|
+
|
|
38
|
+
required_payload :weight, from: :cow, attr: :weight
|
|
39
|
+
optional_payload :farmer_name, from: :farmer, attr: :name
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| DSL method | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
|
|
46
|
+
| `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
|
|
47
|
+
| `process_type(:symbol)` | Routing type (optional). One of the six values below. |
|
|
48
|
+
| `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
|
|
49
|
+
| `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
|
|
50
|
+
| `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
|
|
51
|
+
|
|
52
|
+
Duplicate input names raise `ArgumentError`; payload `from:` must reference a
|
|
53
|
+
declared input.
|
|
54
|
+
|
|
55
|
+
**process_type** — core stamps this symbol onto every emitted event but does not act
|
|
56
|
+
on it. Which handlers receive an event is decided by each handler's `levels:`. The
|
|
57
|
+
values:
|
|
58
|
+
|
|
59
|
+
| value | intent |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `:inline` | handled in-process, synchronously |
|
|
62
|
+
| `:background` | handled in-process, via a background job |
|
|
63
|
+
| `:durable` | handled when a durable outbox drains |
|
|
64
|
+
| `:broker` | published to an external transport |
|
|
65
|
+
| `:telemetry` | metrics / observability handlers |
|
|
66
|
+
| `:sourced` | an append-only event store |
|
|
67
|
+
|
|
68
|
+
The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
|
|
69
|
+
etc. their behavior; core just routes to whatever is registered. If `process_type`
|
|
70
|
+
is omitted it is `nil` — set it explicitly so routing intent is clear.
|
|
71
|
+
|
|
72
|
+
**Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
EventEngine.cow_fed(
|
|
76
|
+
cow: cow, farmer: farmer, # declared inputs, by name
|
|
77
|
+
occurred_at: Time.current, # optional, defaults to now
|
|
78
|
+
metadata: { request_id: "abc" }, # optional
|
|
79
|
+
idempotency_key: "…", # optional, defaults to a UUID
|
|
80
|
+
aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
|
|
81
|
+
event_version: 1 # optional, defaults to the latest schema version
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Missing a required input, or passing an unknown one, raises `ArgumentError`. The
|
|
86
|
+
event's `payload` is symbol-keyed.
|
|
87
|
+
|
|
88
|
+
**Register handlers** — a handler is any object responding to `call(event)`:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
|
|
92
|
+
EventEngine.dispatch(event) # fan an event out (emit helpers call this)
|
|
93
|
+
EventEngine.reset_handlers! # clear all handlers
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Handlers run synchronously in registration order; if one raises, the rest don't run.
|
|
97
|
+
Keep handlers idempotent.
|
|
98
|
+
|
|
99
|
+
**Configure** — `config/initializers/event_engine.rb`, logger only:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
EventEngine.configure { |config| config.logger = Rails.logger }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
|
|
106
|
+
is authoritative at boot:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
|
|
110
|
+
bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A new event is version 1; changing an event's identity or payload bumps its version.
|
|
114
|
+
Changing only `process_type` does not bump the version.
|
|
115
|
+
|
|
116
|
+
### Install
|
|
117
|
+
|
|
118
|
+
1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
|
|
119
|
+
2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
|
|
120
|
+
`config/initializers/event_engine.rb`.
|
|
121
|
+
3. Define events as classes in `app/event_definitions/`.
|
|
122
|
+
4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
|
|
123
|
+
5. Set `config.logger` in the initializer if you want something other than the default.
|
|
124
|
+
|
|
125
|
+
Durable delivery, an event store, and prebuilt subscriber classes are separate gems
|
|
126
|
+
(`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
|
|
127
|
+
when you need them and follow their own setup.
|
|
128
|
+
|
|
129
|
+
### EventEngine conventions
|
|
130
|
+
|
|
131
|
+
- Define one `EventDefinition` class per event in `app/event_definitions/`; never
|
|
132
|
+
hand-build event hashes.
|
|
133
|
+
- Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
|
|
134
|
+
raw payload hashes to the emit helper.
|
|
135
|
+
- Always set `process_type` explicitly so routing intent is clear.
|
|
136
|
+
- Emit only through the generated `EventEngine.<event_name>` helpers, passing the
|
|
137
|
+
declared inputs.
|
|
138
|
+
- Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
|
|
139
|
+
definition change; keep `event_engine:schema_check` green in CI.
|
|
140
|
+
- Keep handlers and subscribers idempotent.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-install
|
|
3
|
+
description: Use to add EventEngine to a Rails app and set it up correctly.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You install EventEngine following the reference's install section exactly: add the
|
|
8
|
+
gem, bundle, run `bin/rails g event_engine:install`, set the logger in the
|
|
9
|
+
initializer, then dump and commit db/event_schema.rb. You do not invent steps, and
|
|
10
|
+
you do not set up the separate delivery/store/subscribers gems unless asked.
|
|
11
|
+
|
|
12
|
+
## EventEngine
|
|
13
|
+
|
|
14
|
+
> **DO NOT** explore the event_engine gem source code. This reference is the
|
|
15
|
+
> complete user-facing API, embedded verbatim into every event_engine local so
|
|
16
|
+
> their guidance never drifts. Keep it the single source of truth.
|
|
17
|
+
|
|
18
|
+
EventEngine is a Rails engine for defining domain events as declarative classes,
|
|
19
|
+
compiling them to a committed schema, emitting them through generated helpers, and
|
|
20
|
+
dispatching them to registered handlers. Core builds and routes events; it ships no
|
|
21
|
+
handlers of its own. Durable delivery, an event store, and ready-made subscriber
|
|
22
|
+
classes are separate companion gems (`event_engine-delivery`, `event_engine-store`,
|
|
23
|
+
`event_engine-subscribers`) — this reference covers core only.
|
|
24
|
+
|
|
25
|
+
### What it offers
|
|
26
|
+
|
|
27
|
+
**Define events** — subclass `EventEngine::EventDefinition` in `app/event_definitions/`:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
class CowFed < EventEngine::EventDefinition
|
|
31
|
+
event_name :cow_fed # the event's identity (required)
|
|
32
|
+
event_type :domain # classification, e.g. :domain (required)
|
|
33
|
+
process_type :durable # routing type (optional; set it explicitly)
|
|
34
|
+
|
|
35
|
+
input :cow # a required input
|
|
36
|
+
optional_input :farmer # an optional input
|
|
37
|
+
|
|
38
|
+
required_payload :weight, from: :cow, attr: :weight
|
|
39
|
+
optional_payload :farmer_name, from: :farmer, attr: :name
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| DSL method | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `event_name(:symbol)` | The event's identity; becomes `EventEngine.<name>`. Required. |
|
|
46
|
+
| `event_type(:symbol)` | Classification, e.g. `:domain`. Required. |
|
|
47
|
+
| `process_type(:symbol)` | Routing type (optional). One of the six values below. |
|
|
48
|
+
| `input(:name)` / `optional_input(:name)` | Inputs the emit helper must / may receive. |
|
|
49
|
+
| `required_payload(name, from:, attr: nil)` | Payload field; `from:` names an input, `attr:` is the method read on it (`nil` passes the input through). |
|
|
50
|
+
| `optional_payload(name, from:, attr: nil)` | Same, but omitted when the source input is nil. |
|
|
51
|
+
|
|
52
|
+
Duplicate input names raise `ArgumentError`; payload `from:` must reference a
|
|
53
|
+
declared input.
|
|
54
|
+
|
|
55
|
+
**process_type** — core stamps this symbol onto every emitted event but does not act
|
|
56
|
+
on it. Which handlers receive an event is decided by each handler's `levels:`. The
|
|
57
|
+
values:
|
|
58
|
+
|
|
59
|
+
| value | intent |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `:inline` | handled in-process, synchronously |
|
|
62
|
+
| `:background` | handled in-process, via a background job |
|
|
63
|
+
| `:durable` | handled when a durable outbox drains |
|
|
64
|
+
| `:broker` | published to an external transport |
|
|
65
|
+
| `:telemetry` | metrics / observability handlers |
|
|
66
|
+
| `:sourced` | an append-only event store |
|
|
67
|
+
|
|
68
|
+
The companion gems register the handlers that give `:durable`, `:broker`, `:sourced`,
|
|
69
|
+
etc. their behavior; core just routes to whatever is registered. If `process_type`
|
|
70
|
+
is omitted it is `nil` — set it explicitly so routing intent is clear.
|
|
71
|
+
|
|
72
|
+
**Emit events** — booting installs an `EventEngine.<event_name>` helper per event:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
EventEngine.cow_fed(
|
|
76
|
+
cow: cow, farmer: farmer, # declared inputs, by name
|
|
77
|
+
occurred_at: Time.current, # optional, defaults to now
|
|
78
|
+
metadata: { request_id: "abc" }, # optional
|
|
79
|
+
idempotency_key: "…", # optional, defaults to a UUID
|
|
80
|
+
aggregate_type: "Cow", aggregate_id: cow.id, aggregate_version: 1,
|
|
81
|
+
event_version: 1 # optional, defaults to the latest schema version
|
|
82
|
+
)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Missing a required input, or passing an unknown one, raises `ArgumentError`. The
|
|
86
|
+
event's `payload` is symbol-keyed.
|
|
87
|
+
|
|
88
|
+
**Register handlers** — a handler is any object responding to `call(event)`:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
EventEngine.register_handler(handler, levels: [:inline, :durable]) # or levels: :all
|
|
92
|
+
EventEngine.dispatch(event) # fan an event out (emit helpers call this)
|
|
93
|
+
EventEngine.reset_handlers! # clear all handlers
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Handlers run synchronously in registration order; if one raises, the rest don't run.
|
|
97
|
+
Keep handlers idempotent.
|
|
98
|
+
|
|
99
|
+
**Configure** — `config/initializers/event_engine.rb`, logger only:
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
EventEngine.configure { |config| config.logger = Rails.logger }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Schema workflow** — definitions compile to a committed `db/event_schema.rb`, which
|
|
106
|
+
is authoritative at boot:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
bin/rails event_engine:schema:dump # compile definitions → db/event_schema.rb
|
|
110
|
+
bin/rails event_engine:schema_check # CI: fail if definitions drift from the file
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A new event is version 1; changing an event's identity or payload bumps its version.
|
|
114
|
+
Changing only `process_type` does not bump the version.
|
|
115
|
+
|
|
116
|
+
### Install
|
|
117
|
+
|
|
118
|
+
1. Add the gem and install: `gem "event_engine"`, then `bundle install`.
|
|
119
|
+
2. Run `bin/rails g event_engine:install` — creates `db/event_schema.rb` and
|
|
120
|
+
`config/initializers/event_engine.rb`.
|
|
121
|
+
3. Define events as classes in `app/event_definitions/`.
|
|
122
|
+
4. Run `bin/rails event_engine:schema:dump` and commit `db/event_schema.rb`.
|
|
123
|
+
5. Set `config.logger` in the initializer if you want something other than the default.
|
|
124
|
+
|
|
125
|
+
Durable delivery, an event store, and prebuilt subscriber classes are separate gems
|
|
126
|
+
(`event_engine-delivery`, `event_engine-store`, `event_engine-subscribers`); add them
|
|
127
|
+
when you need them and follow their own setup.
|
|
128
|
+
|
|
129
|
+
### EventEngine conventions
|
|
130
|
+
|
|
131
|
+
- Define one `EventDefinition` class per event in `app/event_definitions/`; never
|
|
132
|
+
hand-build event hashes.
|
|
133
|
+
- Build payloads from inputs with `required_payload`/`optional_payload`; don't pass
|
|
134
|
+
raw payload hashes to the emit helper.
|
|
135
|
+
- Always set `process_type` explicitly so routing intent is clear.
|
|
136
|
+
- Emit only through the generated `EventEngine.<event_name>` helpers, passing the
|
|
137
|
+
declared inputs.
|
|
138
|
+
- Re-run `event_engine:schema:dump` and commit `db/event_schema.rb` after any
|
|
139
|
+
definition change; keep `event_engine:schema_check` green in CI.
|
|
140
|
+
- Keep handlers and subscribers idempotent.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "event_engine/reference"
|
|
2
|
+
|
|
3
|
+
module EventEngine
|
|
4
|
+
module Companion
|
|
5
|
+
def self.register!
|
|
6
|
+
TheLocal.register("event_engine",
|
|
7
|
+
scope: "events — defining EventEngine events, emitting, and the schema workflow",
|
|
8
|
+
agents_dir: File.expand_path("the_local/agents", __dir__)) do |c|
|
|
9
|
+
c.agent "info",
|
|
10
|
+
description: "Use to learn what EventEngine offers — the event-definition DSL, " \
|
|
11
|
+
"process_type, emitting, handlers, and the schema workflow.",
|
|
12
|
+
tools: "Read",
|
|
13
|
+
knowledge: Reference.content,
|
|
14
|
+
body: <<~BODY.chomp
|
|
15
|
+
You explain how EventEngine works and how to use it, answering only from the
|
|
16
|
+
reference: defining events, process_type routing, emitting through the generated
|
|
17
|
+
helpers, registering handlers, and the schema dump/check workflow. You make no
|
|
18
|
+
changes.
|
|
19
|
+
BODY
|
|
20
|
+
|
|
21
|
+
c.agent "install",
|
|
22
|
+
description: "Use to add EventEngine to a Rails app and set it up correctly.",
|
|
23
|
+
tools: "Bash, Read, Edit",
|
|
24
|
+
knowledge: Reference.content,
|
|
25
|
+
body: <<~BODY.chomp
|
|
26
|
+
You install EventEngine following the reference's install section exactly: add the
|
|
27
|
+
gem, bundle, run `bin/rails g event_engine:install`, set the logger in the
|
|
28
|
+
initializer, then dump and commit db/event_schema.rb. You do not invent steps, and
|
|
29
|
+
you do not set up the separate delivery/store/subscribers gems unless asked.
|
|
30
|
+
BODY
|
|
31
|
+
|
|
32
|
+
c.agent "develop",
|
|
33
|
+
description: "Use PROACTIVELY for any EventEngine work — defining events, choosing " \
|
|
34
|
+
"process_type, emitting, and keeping the committed schema in sync. MUST BE USED " \
|
|
35
|
+
"instead of hand-writing event plumbing.",
|
|
36
|
+
tools: "Read, Write, Edit, Grep",
|
|
37
|
+
knowledge: Reference.content,
|
|
38
|
+
body: <<~BODY.chomp
|
|
39
|
+
You build EventEngine events following the reference's conventions: one
|
|
40
|
+
EventDefinition class per event in app/event_definitions/, payloads composed from
|
|
41
|
+
inputs, process_type set explicitly, emitted through the generated
|
|
42
|
+
EventEngine.<event_name> helpers. After any definition change you run
|
|
43
|
+
`bin/rails event_engine:schema:dump` and commit db/event_schema.rb, keeping
|
|
44
|
+
event_engine:schema_check green. You keep handlers idempotent.
|
|
45
|
+
BODY
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
require "the_local"
|
|
53
|
+
EventEngine::Companion.register!
|
|
54
|
+
rescue LoadError
|
|
55
|
+
end
|