event_engine-store 0.1.0 → 0.1.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 +10 -0
- data/README.md +21 -13
- data/Rakefile +2 -0
- data/db/migrate/20260605000001_create_event_engine_store_stored_events.rb +1 -1
- data/lib/event_engine/store/version.rb +1 -1
- data/the_local/agents/event_engine-store-develop.md +165 -0
- data/the_local/agents/event_engine-store-info.md +62 -0
- data/the_local/agents/event_engine-store-install.md +94 -0
- data/the_local/interface.yml +22 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 11e8057d3dfa1adb99b82eb9e6add21d9f5bfc3fd5a06895ba7e391d136e11ae
|
|
4
|
+
data.tar.gz: 60c1108aef5cae04c14b1b9f77baae03c98a0a8628bba364da51ad267359ee02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5356e1efded2136e940c183b4f7dc84d02c7fe74929d4a60c81d3efeb961992b7de81818ee7c09576c0cec265e6a3a99c088f36d39e8587a819f0c3ac55789a6
|
|
7
|
+
data.tar.gz: 7823ee9ae1e1d3f2ded90f5c664a2e6996bded30620e8e4d52273814b211702b31f4fad868e1f5816b182601656fac4cbd0e59f9efdc4112bb4995cce50fa7e2
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ All notable changes to this project are documented here, following
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.1] - 2026-11-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- The gem ships its expert agents, so an app that installs it can ask an event_engine-store expert how events are stored, replayed and built into projections.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- The stored events migration runs on Rails 7.1.
|
|
18
|
+
|
|
9
19
|
## [0.1.0] - 2026-07-29
|
|
10
20
|
|
|
11
21
|
First published release of `event_engine-store`, the durable event record for the
|
data/README.md
CHANGED
|
@@ -67,14 +67,14 @@ itself up at boot. Once migrated, every dispatched event is recorded automatical
|
|
|
67
67
|
|
|
68
68
|
## How it hooks into the core
|
|
69
69
|
|
|
70
|
-
At Rails boot the engine registers **two** handlers with the core, for
|
|
70
|
+
At Rails boot the engine registers **two** handlers with the core, for every process type:
|
|
71
71
|
|
|
72
72
|
```ruby
|
|
73
73
|
# lib/event_engine/store/engine.rb
|
|
74
74
|
initializer "event_engine.store.register_recorder" do
|
|
75
75
|
config.after_initialize do
|
|
76
|
-
EventEngine.register_handler(Recorder.new,
|
|
77
|
-
EventEngine.register_handler(ProjectionDispatcher.new,
|
|
76
|
+
EventEngine.register_handler(Recorder.new, process_types: :all)
|
|
77
|
+
EventEngine.register_handler(ProjectionDispatcher.new, process_types: :all)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
```
|
|
@@ -85,9 +85,10 @@ So on every `EventEngine.<event>` call:
|
|
|
85
85
|
2. `ProjectionDispatcher#call(event)` calls `apply(event)` on each registered
|
|
86
86
|
projection.
|
|
87
87
|
|
|
88
|
-
> Because both register at `
|
|
89
|
-
>
|
|
90
|
-
>
|
|
88
|
+
> Because both register at `process_types: :all`, the store records events of
|
|
89
|
+
> **every** process type, whichever processor `config/event_rules.yml` routes them to.
|
|
90
|
+
> That's by design: the store is your record of what happened, independent of how it
|
|
91
|
+
> was processed. If you only want to record some
|
|
91
92
|
> events, see [Recording only some events](#recording-only-some-events).
|
|
92
93
|
|
|
93
94
|
---
|
|
@@ -103,7 +104,7 @@ persisted records), so an attempt to update raises `ActiveRecord::ReadOnlyRecord
|
|
|
103
104
|
| `event_name` | string | ✓ | NOT NULL |
|
|
104
105
|
| `event_type` | string | | classification |
|
|
105
106
|
| `event_version` | integer | | schema version |
|
|
106
|
-
| `
|
|
107
|
+
| `process_type` | string | | processor the event was routed to |
|
|
107
108
|
| `payload` | json | | event data |
|
|
108
109
|
| `metadata` | json | | context (actor, request id, …) |
|
|
109
110
|
| `occurred_at` | datetime | ✓ | logical event time |
|
|
@@ -120,7 +121,7 @@ create_table :event_engine_store_stored_events do |t|
|
|
|
120
121
|
t.string :event_name, null: false
|
|
121
122
|
t.string :event_type
|
|
122
123
|
t.integer :event_version
|
|
123
|
-
t.
|
|
124
|
+
t.string :process_type
|
|
124
125
|
t.json :payload
|
|
125
126
|
t.json :metadata
|
|
126
127
|
t.datetime :occurred_at
|
|
@@ -212,7 +213,7 @@ module EventEngine
|
|
|
212
213
|
event_name: event.event_name,
|
|
213
214
|
event_type: event.event_type,
|
|
214
215
|
event_version: event.event_version,
|
|
215
|
-
|
|
216
|
+
process_type: event.process_type,
|
|
216
217
|
payload: event.payload,
|
|
217
218
|
metadata: event.metadata,
|
|
218
219
|
occurred_at: event.occurred_at,
|
|
@@ -293,7 +294,7 @@ EventEngine::Store::Recorder.prepend(RecordDomainOnly)
|
|
|
293
294
|
```
|
|
294
295
|
|
|
295
296
|
**Why:** keep the log focused on domain/audit-worthy events and avoid recording noisy
|
|
296
|
-
|
|
297
|
+
system events.
|
|
297
298
|
|
|
298
299
|
### Replacing the recorder entirely
|
|
299
300
|
|
|
@@ -308,7 +309,7 @@ class MyAuditRecorder
|
|
|
308
309
|
end
|
|
309
310
|
|
|
310
311
|
Rails.application.config.after_initialize do
|
|
311
|
-
EventEngine.register_handler(MyAuditRecorder.new,
|
|
312
|
+
EventEngine.register_handler(MyAuditRecorder.new, process_types: :all)
|
|
312
313
|
end
|
|
313
314
|
```
|
|
314
315
|
|
|
@@ -362,7 +363,7 @@ log in append order (ordered by `id`, batched via `find_each`):
|
|
|
362
363
|
|
|
363
364
|
```ruby
|
|
364
365
|
EventEngine::Store::Replay.each do |event|
|
|
365
|
-
# event is
|
|
366
|
+
# event is an EventEngine::Event rebuilt from its row
|
|
366
367
|
puts "#{event.occurred_at} #{event.event_name}"
|
|
367
368
|
end
|
|
368
369
|
|
|
@@ -371,6 +372,13 @@ enum = EventEngine::Store::Replay.each
|
|
|
371
372
|
enum.count
|
|
372
373
|
```
|
|
373
374
|
|
|
375
|
+
A replayed event differs from the one that was dispatched:
|
|
376
|
+
|
|
377
|
+
- `payload` and `metadata` have **string keys**, because they are read back from JSON
|
|
378
|
+
columns (`event.payload["weight"]`, not `event.payload[:weight]`).
|
|
379
|
+
- `event_name` and `process_type` are **strings**, read back from string columns.
|
|
380
|
+
- `subject` and `domain` are **nil**, because the table has no columns for them.
|
|
381
|
+
|
|
374
382
|
Use replay to rebuild read models, backfill a new projection, or audit history.
|
|
375
383
|
|
|
376
384
|
---
|
|
@@ -386,7 +394,7 @@ class OrdersByDay
|
|
|
386
394
|
attr_reader :counts
|
|
387
395
|
|
|
388
396
|
def apply(event)
|
|
389
|
-
@counts[event.occurred_at.to_date] += 1 if event.event_name ==
|
|
397
|
+
@counts[event.occurred_at.to_date] += 1 if event.event_name.to_s == "order_placed"
|
|
390
398
|
end
|
|
391
399
|
end
|
|
392
400
|
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-store-develop
|
|
3
|
+
description: Use PROACTIVELY for reading an app's recorded event history on event_engine-store — querying the stored-events log, replaying it in order, writing and registering projections, rebuilding a read model from the log, and changing which events get recorded — MUST BE USED instead of hand-rolling an audit table, an event-history query, or a script that recomputes a read model from scratch.
|
|
4
|
+
tools: Read, Write, Edit, Grep
|
|
5
|
+
scope: event store — recording every dispatched event to an append-only table, replaying the log in order, and rebuilding projections from it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local builds on a host app's event record with event_engine-store, following
|
|
9
|
+
the steps below in order. Where a step names a decision, it asks the developer
|
|
10
|
+
instead of choosing.
|
|
11
|
+
|
|
12
|
+
## What event_engine-store is
|
|
13
|
+
|
|
14
|
+
A Rails engine that writes every event event_engine dispatches to an append-only
|
|
15
|
+
table, and reads that table back as events. Fire this local when an app needs its
|
|
16
|
+
event history queried, a read model kept up to date from events, a read model
|
|
17
|
+
rebuilt or backfilled from past events, or a change to what the log records.
|
|
18
|
+
|
|
19
|
+
## Interface
|
|
20
|
+
|
|
21
|
+
- `EventEngine::Store::StoredEvent` — the ActiveRecord model for the log, one row
|
|
22
|
+
per dispatched event. Query it like any model; a saved row cannot be changed.
|
|
23
|
+
- `EventEngine::Store::Replay.each` — yields every recorded event, oldest first,
|
|
24
|
+
as an event object. Without a block it returns an Enumerator.
|
|
25
|
+
- `EventEngine::Store.register_projection` — adds a projection that receives
|
|
26
|
+
every event as it is dispatched.
|
|
27
|
+
- `EventEngine::Store.rebuild` — replays the whole log into one projection.
|
|
28
|
+
- `EventEngine::Store.projections` — the list of registered projections.
|
|
29
|
+
- `EventEngine::Store.reset_projections!` — removes every registered projection.
|
|
30
|
+
- `EventEngine::Store::Recorder` — the handler that writes each dispatched event
|
|
31
|
+
to the log. Prepend a module to it to change what is recorded.
|
|
32
|
+
|
|
33
|
+
## How to use it
|
|
34
|
+
|
|
35
|
+
The `event_engine_store_stored_events` table must exist in the host app. If it
|
|
36
|
+
does not, hand off to `event_engine-store-install` first and come back.
|
|
37
|
+
|
|
38
|
+
1. Query the log with plain ActiveRecord on `EventEngine::Store::StoredEvent`. Its
|
|
39
|
+
columns are `event_name`, `event_type`, `event_version`, `process_type`,
|
|
40
|
+
`payload`, `metadata`, `occurred_at`, `idempotency_key`, `aggregate_type`,
|
|
41
|
+
`aggregate_id`, `aggregate_version`, and `created_at`. `event_name`,
|
|
42
|
+
`occurred_at`, and `idempotency_key` are indexed.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
EventEngine::Store::StoredEvent.where(aggregate_type: "Order", aggregate_id: order.id.to_s).order(:id)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The model has no scopes. To add some, decorate it in an initializer inside
|
|
49
|
+
`Rails.application.config.to_prepare`, so the decoration is reapplied when
|
|
50
|
+
Rails reloads the model:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
Rails.application.config.to_prepare do
|
|
54
|
+
EventEngine::Store::StoredEvent.class_eval do
|
|
55
|
+
scope :named, ->(name) { where(event_name: name.to_s) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
2. Replay the log when you need events back as event objects rather than rows:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
EventEngine::Store::Replay.each { |event| puts "#{event.occurred_at} #{event.event_name}" }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Events come back in insertion order, loaded in batches. The Enumerator returned
|
|
67
|
+
without a block runs its query each time it is iterated, so it sees rows
|
|
68
|
+
written after it was created.
|
|
69
|
+
|
|
70
|
+
3. Write a projection. A projection is any object that responds to
|
|
71
|
+
`apply(event)`. Ask the developer what read model it maintains and where that
|
|
72
|
+
state lives (memory, a table, a cache) — there is no default.
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class OrdersPerDay
|
|
76
|
+
attr_reader :counts
|
|
77
|
+
|
|
78
|
+
def initialize
|
|
79
|
+
@counts = Hash.new(0)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def apply(event)
|
|
83
|
+
@counts[event.occurred_at.to_date] += 1 if event.event_name.to_s == "order_placed"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
4. Register it for live updates, once per process, from an initializer:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
Rails.application.config.after_initialize do
|
|
92
|
+
EventEngine::Store.register_projection(OrdersPerDay.new)
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
From then on it receives every dispatched event, of every process type, after
|
|
97
|
+
the event has been written to the log. Registered projections run in
|
|
98
|
+
registration order.
|
|
99
|
+
|
|
100
|
+
5. Rebuild when the projection's state must reflect history — a new projection, a
|
|
101
|
+
changed `apply`, or lost state:
|
|
102
|
+
|
|
103
|
+
```ruby
|
|
104
|
+
EventEngine::Store.rebuild(projection)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
It calls `apply` once per recorded event, oldest first. It does not clear the
|
|
108
|
+
projection first, and the projection does not need to be registered. Ask the
|
|
109
|
+
developer whether rebuilding should start from a fresh projection or cleared
|
|
110
|
+
state, and whether it can run while events are still being emitted — an event
|
|
111
|
+
emitted during a rebuild of a registered projection can reach it twice.
|
|
112
|
+
|
|
113
|
+
6. In tests, call `EventEngine::Store.reset_projections!` in teardown after
|
|
114
|
+
registering a projection, and read `EventEngine::Store.projections` to assert
|
|
115
|
+
on what is registered.
|
|
116
|
+
|
|
117
|
+
7. Change what is recorded only if the developer asks for it, by prepending a
|
|
118
|
+
module to `EventEngine::Store::Recorder` from an initializer. Its `call(event)`
|
|
119
|
+
returns the created row. Ask which change they want:
|
|
120
|
+
|
|
121
|
+
- Record a subset — return early for events to skip, otherwise call `super`.
|
|
122
|
+
- Never let recording break an emit — rescue around `super`, log, return
|
|
123
|
+
`nil`. A failed write is then lost unless it is retried.
|
|
124
|
+
- Record extra columns — add them with a host migration, then have `call`
|
|
125
|
+
create the row itself with the default attributes plus the new ones. The
|
|
126
|
+
default recorder writes only its fixed attributes, so new columns stay
|
|
127
|
+
`NULL` otherwise.
|
|
128
|
+
- Stop recording entirely — define `call(_event)` with an empty body.
|
|
129
|
+
Registered projections still run.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
module RecordOrdersOnly
|
|
133
|
+
def call(event)
|
|
134
|
+
return unless event.event_name.to_s.start_with?("order_")
|
|
135
|
+
|
|
136
|
+
super
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
EventEngine::Store::Recorder.prepend(RecordOrdersOnly)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Conventions
|
|
144
|
+
|
|
145
|
+
- Never update or destroy a saved `StoredEvent`. `update` and `destroy` on a saved
|
|
146
|
+
row raise `ActiveRecord::ReadOnlyRecord`. Correct history by emitting a new
|
|
147
|
+
event.
|
|
148
|
+
- Recording and projections run synchronously inside the emit call. A database
|
|
149
|
+
error while recording, or an exception raised by `apply`, propagates to the
|
|
150
|
+
code that emitted the event and stops later handlers and projections. Keep
|
|
151
|
+
`apply` fast; enqueue a job for heavy work.
|
|
152
|
+
- A replayed event differs from a live one. `event_name` comes back as a string,
|
|
153
|
+
`payload` and `metadata` come back with string keys, and `subject` and `domain`
|
|
154
|
+
are `nil` because they are not stored. Write `apply` to accept both: compare
|
|
155
|
+
`event_name.to_s`, and read payload keys as strings or with indifferent access.
|
|
156
|
+
- `register_projection` keeps a plain in-memory list. Registering the same
|
|
157
|
+
projection twice applies every event to it twice, and each process registers
|
|
158
|
+
its own.
|
|
159
|
+
- The idempotency key is not unique, so the same event emitted twice is recorded
|
|
160
|
+
twice. A projection that must count each event once has to deduplicate itself.
|
|
161
|
+
- Do not call `EventEngine.reset_handlers!` to stop recording. It removes every
|
|
162
|
+
handler, including the one that feeds registered projections.
|
|
163
|
+
- Out of scope for this local: adding the gem and creating the table
|
|
164
|
+
(`event_engine-store-install`), and defining, routing, or emitting events
|
|
165
|
+
(`event_engine-develop`).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-store-info
|
|
3
|
+
description: Use to learn what event_engine-store offers — the permanent append-only record of every dispatched event, replay of that record in order, and projections kept live and rebuilt from it.
|
|
4
|
+
tools: Read
|
|
5
|
+
scope: event store — recording every dispatched event to an append-only table, replaying the log in order, and rebuilding projections from it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local explains event_engine-store and the vocabulary it uses. It changes
|
|
9
|
+
nothing and gives no steps — read it to orient, then go to the local that owns the
|
|
10
|
+
work.
|
|
11
|
+
|
|
12
|
+
## What event_engine-store is
|
|
13
|
+
|
|
14
|
+
event_engine-store is a Rails engine that adds a permanent record to an app built
|
|
15
|
+
on event_engine. Once installed, every event event_engine dispatches is written as
|
|
16
|
+
one row to a table the host app owns, whatever its process type. Rows are never
|
|
17
|
+
updated: the table is the history of what happened, in the order it happened.
|
|
18
|
+
|
|
19
|
+
Reach for it when an app needs to answer "what happened, and when" from a durable
|
|
20
|
+
log, rebuild a read model (counters, dashboards, search indexes) from history, or
|
|
21
|
+
backfill a new read model from events recorded before it existed. It does not
|
|
22
|
+
aggregate or compute metrics, and it does not deliver events anywhere — it records
|
|
23
|
+
them and reads them back.
|
|
24
|
+
|
|
25
|
+
## Interface
|
|
26
|
+
|
|
27
|
+
This local documents no commands — it is background only.
|
|
28
|
+
|
|
29
|
+
The `event_engine-store-install` local owns adding the gem to a host app and
|
|
30
|
+
creating its table. The `event_engine-store-develop` local owns everything an app
|
|
31
|
+
does with the record: querying it, replaying it, registering and rebuilding
|
|
32
|
+
projections, and changing what gets recorded.
|
|
33
|
+
|
|
34
|
+
## How to use it
|
|
35
|
+
|
|
36
|
+
One decision: is the store already in the app?
|
|
37
|
+
|
|
38
|
+
- Adding the gem to an app for the first time, or the table is missing →
|
|
39
|
+
`event_engine-store-install`.
|
|
40
|
+
- Querying the log, replaying it, writing a projection, or changing what is
|
|
41
|
+
recorded → `event_engine-store-develop`.
|
|
42
|
+
|
|
43
|
+
If you only needed the vocabulary, you have it — stop here.
|
|
44
|
+
|
|
45
|
+
## Conventions
|
|
46
|
+
|
|
47
|
+
- **Stored event** — one row in the log, holding one dispatched event's name,
|
|
48
|
+
type, version, process type, payload, metadata, occurred-at time, idempotency
|
|
49
|
+
key, and aggregate type / id / version. The event's subject and domain are not
|
|
50
|
+
stored.
|
|
51
|
+
- **Append-only** — a row is written once and never changed. Order is the row's
|
|
52
|
+
insertion order.
|
|
53
|
+
- **Recording** — happens automatically for every dispatched event, inside the
|
|
54
|
+
emit call, with no configuration.
|
|
55
|
+
- **Replay** — reading the log back from the first row to the last, as event
|
|
56
|
+
objects.
|
|
57
|
+
- **Projection** — an object with an `apply(event)` method that builds a read
|
|
58
|
+
model from events. A registered projection receives each event as it is
|
|
59
|
+
dispatched; any projection can be rebuilt by replaying the whole log into it.
|
|
60
|
+
- **Companion gems** — event_engine (required) defines and dispatches events.
|
|
61
|
+
event_engine-delivery keeps a temporary outbox for delivery; the two can run
|
|
62
|
+
side by side.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: event_engine-store-install
|
|
3
|
+
description: Use to hook event_engine-store into a Rails app — adding the gem, copying and running its stored-events migration, and confirming dispatched events are being recorded.
|
|
4
|
+
tools: Bash, Read, Edit
|
|
5
|
+
scope: event store — recording every dispatched event to an append-only table, replaying the log in order, and rebuilding projections from it
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This local follows the steps below exactly, in order, and invents none. Where a
|
|
9
|
+
step names a decision, it asks the developer instead of choosing.
|
|
10
|
+
|
|
11
|
+
## What event_engine-store is
|
|
12
|
+
|
|
13
|
+
A Rails engine that records every event event_engine dispatches into an
|
|
14
|
+
append-only table in the host app — hook it in when an app needs a permanent,
|
|
15
|
+
replayable log of its events.
|
|
16
|
+
|
|
17
|
+
## Interface
|
|
18
|
+
|
|
19
|
+
- `bin/rails event_engine_store:install:migrations` — copies the migration that creates the
|
|
20
|
+
`event_engine_store_stored_events` table into the host app's `db/migrate/`.
|
|
21
|
+
|
|
22
|
+
## How to use it
|
|
23
|
+
|
|
24
|
+
1. Confirm event_engine is already installed in the host app — its initializer
|
|
25
|
+
exists and the app boots with its schema catalog. If not, hand off to the
|
|
26
|
+
`event_engine-install` local first and come back. This gem requires
|
|
27
|
+
`event_engine` 0.2.1 or later.
|
|
28
|
+
|
|
29
|
+
2. Add the gem to the host app's `Gemfile`:
|
|
30
|
+
|
|
31
|
+
```ruby
|
|
32
|
+
gem "event_engine-store"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Ask the developer whether to take the released gem, pin a version, or track the
|
|
36
|
+
source repository (`github: "DYB-Development/event_engine-store"`) — do not
|
|
37
|
+
pick for them. The host must be a Rails app; this gem is a Rails engine.
|
|
38
|
+
|
|
39
|
+
3. Install it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
bundle install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
4. Copy the migration into the host app:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bin/rails event_engine_store:install:migrations
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It writes one new file under `db/migrate/` that creates
|
|
52
|
+
`event_engine_store_stored_events`. The command copies pending migrations from
|
|
53
|
+
every installed engine, not only this one — show the developer the list of new
|
|
54
|
+
files before continuing.
|
|
55
|
+
|
|
56
|
+
5. Check the host's Rails version. The copied migration is declared as
|
|
57
|
+
`ActiveRecord::Migration[8.1]`, which Rails versions before 8.1 reject. If the
|
|
58
|
+
host runs an older Rails, ask the developer whether to change the bracketed
|
|
59
|
+
version in the copied file to the host's own Rails version — do not change it
|
|
60
|
+
without asking.
|
|
61
|
+
|
|
62
|
+
6. Run the migration and commit both the migration and the updated schema file:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bin/rails db:migrate
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
7. Verify the table exists:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bin/rails runner 'p ActiveRecord::Base.connection.table_exists?("event_engine_store_stored_events")'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`true` means the install is done. `false` means step 6 did not run against
|
|
75
|
+
this environment's database.
|
|
76
|
+
|
|
77
|
+
8. Verify recording. In a Rails console, note
|
|
78
|
+
`ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM event_engine_store_stored_events")`,
|
|
79
|
+
emit any catalogued event the app already emits, and read the count again. It
|
|
80
|
+
grows by one per dispatched event.
|
|
81
|
+
|
|
82
|
+
## Conventions
|
|
83
|
+
|
|
84
|
+
- There is no generator, no initializer, and no configuration. Once the gem is in
|
|
85
|
+
the bundle and the table exists, every dispatched event is recorded at boot
|
|
86
|
+
without any host code.
|
|
87
|
+
- Recording is synchronous and inside the emit call, so an emit fails if the
|
|
88
|
+
table is missing. Do not deploy the gem to an environment before its migration
|
|
89
|
+
has run there.
|
|
90
|
+
- The table has no foreign keys and no unique index. The idempotency key is
|
|
91
|
+
indexed but not unique, so a duplicate emit is recorded twice.
|
|
92
|
+
- Re-running step 4 copies nothing that is already in `db/migrate/`.
|
|
93
|
+
- Out of scope for this local: querying or replaying the log, projections, and
|
|
94
|
+
changing what gets recorded. All of that belongs to `event_engine-store-develop`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
scope: event store — recording every dispatched event to an append-only table, replaying the log in order, and rebuilding projections from it
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
- bin/rails event_engine_store:install:migrations
|
|
5
|
+
|
|
6
|
+
develop:
|
|
7
|
+
- EventEngine::Store::StoredEvent
|
|
8
|
+
- EventEngine::Store::Replay.each
|
|
9
|
+
- EventEngine::Store.register_projection
|
|
10
|
+
- EventEngine::Store.rebuild
|
|
11
|
+
- EventEngine::Store.projections
|
|
12
|
+
- EventEngine::Store.reset_projections!
|
|
13
|
+
- EventEngine::Store::Recorder
|
|
14
|
+
|
|
15
|
+
sources:
|
|
16
|
+
- lib/event_engine/store.rb
|
|
17
|
+
- lib/event_engine/store/engine.rb
|
|
18
|
+
- lib/event_engine/store/recorder.rb
|
|
19
|
+
- lib/event_engine/store/replay.rb
|
|
20
|
+
- lib/event_engine/store/projection_dispatcher.rb
|
|
21
|
+
- app/models/event_engine/store/stored_event.rb
|
|
22
|
+
- db/migrate/20260605000001_create_event_engine_store_stored_events.rb
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: event_engine-store
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tylercschneider
|
|
@@ -103,6 +103,10 @@ files:
|
|
|
103
103
|
- lib/event_engine/store/replay.rb
|
|
104
104
|
- lib/event_engine/store/version.rb
|
|
105
105
|
- lib/tasks/event_engine/store_tasks.rake
|
|
106
|
+
- the_local/agents/event_engine-store-develop.md
|
|
107
|
+
- the_local/agents/event_engine-store-info.md
|
|
108
|
+
- the_local/agents/event_engine-store-install.md
|
|
109
|
+
- the_local/interface.yml
|
|
106
110
|
homepage: https://github.com/DYB-Development/event_engine-store
|
|
107
111
|
licenses:
|
|
108
112
|
- MIT
|