igniter-ledger 0.5.2
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/README.md +481 -0
- data/examples/intelligent_ledger/availability_boundary_ledger.rb +1190 -0
- data/examples/intelligent_ledger/availability_deriver.rb +150 -0
- data/examples/intelligent_ledger/availability_ledger.rb +197 -0
- data/examples/intelligent_ledger/ledger_boundary.rb +180 -0
- data/examples/store_poc.rb +45 -0
- data/exe/igniter-ledger-server +111 -0
- data/exe/igniter-store-server +6 -0
- data/ext/igniter_store_native/Cargo.toml +28 -0
- data/ext/igniter_store_native/extconf.rb +6 -0
- data/ext/igniter_store_native/src/fact.rs +303 -0
- data/ext/igniter_store_native/src/fact_log.rs +180 -0
- data/ext/igniter_store_native/src/file_backend.rs +91 -0
- data/ext/igniter_store_native/src/lib.rs +55 -0
- data/lib/igniter/ledger.rb +7 -0
- data/lib/igniter/store/access_path.rb +84 -0
- data/lib/igniter/store/change_event.rb +65 -0
- data/lib/igniter/store/changefeed_buffer.rb +585 -0
- data/lib/igniter/store/codecs.rb +253 -0
- data/lib/igniter/store/contractable_receipt_sink.rb +172 -0
- data/lib/igniter/store/fact.rb +121 -0
- data/lib/igniter/store/fact_log.rb +103 -0
- data/lib/igniter/store/file_backend.rb +269 -0
- data/lib/igniter/store/http_adapter.rb +413 -0
- data/lib/igniter/store/igniter_store.rb +838 -0
- data/lib/igniter/store/mcp_adapter.rb +403 -0
- data/lib/igniter/store/native.rb +80 -0
- data/lib/igniter/store/network_backend.rb +159 -0
- data/lib/igniter/store/protocol/handlers/access_path_handler.rb +38 -0
- data/lib/igniter/store/protocol/handlers/command_handler.rb +59 -0
- data/lib/igniter/store/protocol/handlers/derivation_handler.rb +27 -0
- data/lib/igniter/store/protocol/handlers/effect_handler.rb +65 -0
- data/lib/igniter/store/protocol/handlers/history_handler.rb +24 -0
- data/lib/igniter/store/protocol/handlers/projection_handler.rb +41 -0
- data/lib/igniter/store/protocol/handlers/relation_handler.rb +43 -0
- data/lib/igniter/store/protocol/handlers/store_handler.rb +24 -0
- data/lib/igniter/store/protocol/handlers/subscription_handler.rb +24 -0
- data/lib/igniter/store/protocol/interpreter.rb +447 -0
- data/lib/igniter/store/protocol/receipt.rb +96 -0
- data/lib/igniter/store/protocol/sync_profile.rb +53 -0
- data/lib/igniter/store/protocol/wire_envelope.rb +214 -0
- data/lib/igniter/store/protocol.rb +27 -0
- data/lib/igniter/store/read_cache.rb +163 -0
- data/lib/igniter/store/schema_graph.rb +248 -0
- data/lib/igniter/store/segmented_file_backend.rb +699 -0
- data/lib/igniter/store/server_config.rb +55 -0
- data/lib/igniter/store/server_logger.rb +64 -0
- data/lib/igniter/store/server_metrics.rb +222 -0
- data/lib/igniter/store/store_server.rb +597 -0
- data/lib/igniter/store/subscription_registry.rb +73 -0
- data/lib/igniter/store/tbackend_adapter_descriptor.rb +307 -0
- data/lib/igniter/store/tcp_adapter.rb +127 -0
- data/lib/igniter/store/wire_protocol.rb +42 -0
- data/lib/igniter/store.rb +64 -0
- data/lib/igniter-ledger.rb +4 -0
- data/lib/igniter-store.rb +5 -0
- metadata +212 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Store
|
|
7
|
+
module Protocol
|
|
8
|
+
# OP3 — Wire Envelope: process boundary routing for StoreServer.
|
|
9
|
+
#
|
|
10
|
+
# Every request is wrapped in:
|
|
11
|
+
# { protocol: :igniter_store, schema_version: 1,
|
|
12
|
+
# request_id: "req_...", op: :write_fact,
|
|
13
|
+
# packet: { kind: :fact, store: :tasks, key: "t1", value: {} } }
|
|
14
|
+
#
|
|
15
|
+
# Every response is:
|
|
16
|
+
# { protocol: :igniter_store, schema_version: 1,
|
|
17
|
+
# request_id: "req_...",
|
|
18
|
+
# status: :ok | :error,
|
|
19
|
+
# result: { ... } | error: "message" }
|
|
20
|
+
#
|
|
21
|
+
# This layer sits above the CRC32-framed WireProtocol transport and below
|
|
22
|
+
# application-level handlers. It is pure Ruby — no I/O, no sockets.
|
|
23
|
+
# The StoreServer feeds deserialized hashes in and ships serialized responses out.
|
|
24
|
+
class WireEnvelope
|
|
25
|
+
PROTOCOL = :igniter_store
|
|
26
|
+
SCHEMA_VERSION = 1
|
|
27
|
+
|
|
28
|
+
OPERATIONS = %i[
|
|
29
|
+
register_descriptor
|
|
30
|
+
write
|
|
31
|
+
append
|
|
32
|
+
write_fact
|
|
33
|
+
read
|
|
34
|
+
query
|
|
35
|
+
resolve
|
|
36
|
+
causation_chain
|
|
37
|
+
lineage
|
|
38
|
+
fact_ref
|
|
39
|
+
metadata_snapshot
|
|
40
|
+
descriptor_snapshot
|
|
41
|
+
observability_snapshot
|
|
42
|
+
sync_hub_profile
|
|
43
|
+
replay
|
|
44
|
+
storage_stats
|
|
45
|
+
segment_manifest
|
|
46
|
+
compaction_activity
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
def initialize(interpreter)
|
|
50
|
+
@interpreter = interpreter
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Dispatch a single envelope hash.
|
|
54
|
+
# Returns a response envelope hash (never raises).
|
|
55
|
+
def dispatch(envelope)
|
|
56
|
+
envelope = envelope.transform_keys(&:to_sym)
|
|
57
|
+
req_id = envelope[:request_id]
|
|
58
|
+
|
|
59
|
+
proto = envelope[:protocol]&.to_sym
|
|
60
|
+
unless proto == PROTOCOL
|
|
61
|
+
return error_response(req_id, "Unknown protocol: #{proto.inspect}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
op = envelope[:op]&.to_sym
|
|
65
|
+
unless op && OPERATIONS.include?(op)
|
|
66
|
+
return error_response(req_id, "Unknown or missing op: #{op.inspect}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
packet = (envelope[:packet] || {})
|
|
70
|
+
packet = packet.transform_keys(&:to_sym) if packet.is_a?(Hash)
|
|
71
|
+
|
|
72
|
+
result = route(op, packet)
|
|
73
|
+
ok_response(req_id, result)
|
|
74
|
+
rescue => e
|
|
75
|
+
error_response(req_id, "Internal error: #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def route(op, packet)
|
|
81
|
+
case op
|
|
82
|
+
when :register_descriptor
|
|
83
|
+
@interpreter.register(packet)
|
|
84
|
+
|
|
85
|
+
when :write
|
|
86
|
+
@interpreter.write(
|
|
87
|
+
store: packet.fetch(:store),
|
|
88
|
+
key: packet.fetch(:key),
|
|
89
|
+
value: packet.fetch(:value),
|
|
90
|
+
producer: packet[:producer]
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
when :append
|
|
94
|
+
@interpreter.append(
|
|
95
|
+
history: packet.fetch(:history),
|
|
96
|
+
event: packet.fetch(:event),
|
|
97
|
+
key: packet[:key],
|
|
98
|
+
partition_key: packet[:partition_key],
|
|
99
|
+
schema_version: packet.fetch(:schema_version, 1),
|
|
100
|
+
valid_time: packet[:valid_time],
|
|
101
|
+
term: packet[:term],
|
|
102
|
+
producer: packet[:producer],
|
|
103
|
+
derivation: packet[:derivation]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
when :write_fact
|
|
107
|
+
@interpreter.write_fact(packet)
|
|
108
|
+
|
|
109
|
+
when :read
|
|
110
|
+
value = @interpreter.read(
|
|
111
|
+
store: packet.fetch(:store),
|
|
112
|
+
key: packet.fetch(:key),
|
|
113
|
+
as_of: packet[:as_of]
|
|
114
|
+
)
|
|
115
|
+
{ value: value, found: !value.nil? }
|
|
116
|
+
|
|
117
|
+
when :query
|
|
118
|
+
items = @interpreter.query(
|
|
119
|
+
store: packet.fetch(:store),
|
|
120
|
+
where: packet.fetch(:where, {}),
|
|
121
|
+
order: packet[:order],
|
|
122
|
+
limit: packet[:limit],
|
|
123
|
+
as_of: packet[:as_of]
|
|
124
|
+
)
|
|
125
|
+
{ items: items, results: items.map { |item| item[:value] }, count: items.size }
|
|
126
|
+
|
|
127
|
+
when :resolve
|
|
128
|
+
items = @interpreter.resolve_items(
|
|
129
|
+
packet.fetch(:relation).to_sym,
|
|
130
|
+
from: packet.fetch(:from),
|
|
131
|
+
as_of: packet[:as_of]
|
|
132
|
+
)
|
|
133
|
+
{ items: items, results: items.map { |item| item[:value] }, count: items.size }
|
|
134
|
+
|
|
135
|
+
when :causation_chain
|
|
136
|
+
chain = @interpreter.causation_chain(
|
|
137
|
+
store: packet.fetch(:store),
|
|
138
|
+
key: packet.fetch(:key)
|
|
139
|
+
)
|
|
140
|
+
{ chain: chain, count: chain.size }
|
|
141
|
+
|
|
142
|
+
when :lineage
|
|
143
|
+
@interpreter.lineage(
|
|
144
|
+
store: packet.fetch(:store),
|
|
145
|
+
key: packet.fetch(:key)
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
when :fact_ref
|
|
149
|
+
ref = @interpreter.fact_ref(packet.fetch(:fact_id))
|
|
150
|
+
{ found: !ref.nil?, ref: ref }
|
|
151
|
+
|
|
152
|
+
when :metadata_snapshot
|
|
153
|
+
@interpreter.metadata_snapshot
|
|
154
|
+
|
|
155
|
+
when :descriptor_snapshot
|
|
156
|
+
@interpreter.descriptor_snapshot
|
|
157
|
+
|
|
158
|
+
when :observability_snapshot
|
|
159
|
+
@interpreter.observability_snapshot
|
|
160
|
+
|
|
161
|
+
when :sync_hub_profile
|
|
162
|
+
@interpreter.sync_hub_profile(
|
|
163
|
+
as_of: packet[:as_of],
|
|
164
|
+
cursor: packet[:cursor],
|
|
165
|
+
stores: packet[:stores]
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
when :replay
|
|
169
|
+
facts = @interpreter.replay(
|
|
170
|
+
from: packet[:from],
|
|
171
|
+
to: packet[:to],
|
|
172
|
+
filter: packet[:filter]
|
|
173
|
+
)
|
|
174
|
+
{ facts: facts, count: facts.size }
|
|
175
|
+
|
|
176
|
+
when :storage_stats
|
|
177
|
+
@interpreter.storage_stats(store: packet[:store])
|
|
178
|
+
|
|
179
|
+
when :segment_manifest
|
|
180
|
+
@interpreter.segment_manifest(store: packet[:store])
|
|
181
|
+
|
|
182
|
+
when :compaction_activity
|
|
183
|
+
@interpreter.compaction_activity(
|
|
184
|
+
store: packet[:store],
|
|
185
|
+
kind: packet[:kind],
|
|
186
|
+
since: packet[:since],
|
|
187
|
+
limit: packet[:limit]
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ok_response(request_id, result)
|
|
193
|
+
{
|
|
194
|
+
protocol: PROTOCOL,
|
|
195
|
+
schema_version: SCHEMA_VERSION,
|
|
196
|
+
request_id: request_id,
|
|
197
|
+
status: :ok,
|
|
198
|
+
result: result
|
|
199
|
+
}
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def error_response(request_id, message)
|
|
203
|
+
{
|
|
204
|
+
protocol: PROTOCOL,
|
|
205
|
+
schema_version: SCHEMA_VERSION,
|
|
206
|
+
request_id: request_id,
|
|
207
|
+
status: :error,
|
|
208
|
+
error: message
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "protocol/receipt"
|
|
4
|
+
require_relative "protocol/handlers/store_handler"
|
|
5
|
+
require_relative "protocol/handlers/history_handler"
|
|
6
|
+
require_relative "protocol/handlers/access_path_handler"
|
|
7
|
+
require_relative "protocol/handlers/relation_handler"
|
|
8
|
+
require_relative "protocol/handlers/projection_handler"
|
|
9
|
+
require_relative "protocol/handlers/derivation_handler"
|
|
10
|
+
require_relative "protocol/handlers/command_handler"
|
|
11
|
+
require_relative "protocol/handlers/effect_handler"
|
|
12
|
+
require_relative "protocol/handlers/subscription_handler"
|
|
13
|
+
require_relative "protocol/interpreter"
|
|
14
|
+
require_relative "protocol/sync_profile"
|
|
15
|
+
require_relative "protocol/wire_envelope"
|
|
16
|
+
|
|
17
|
+
module Igniter
|
|
18
|
+
module Store
|
|
19
|
+
module Protocol
|
|
20
|
+
# Convenience factory: Protocol.new returns a fresh Interpreter backed by
|
|
21
|
+
# an in-memory IgniterStore. Pass an existing store to wrap it instead.
|
|
22
|
+
def self.new(store = nil)
|
|
23
|
+
Interpreter.new(store || IgniterStore.new)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Igniter
|
|
6
|
+
module Store
|
|
7
|
+
class ReadCache
|
|
8
|
+
include MonitorMixin
|
|
9
|
+
|
|
10
|
+
DEFAULT_LRU_CAP = 1_000
|
|
11
|
+
|
|
12
|
+
def initialize(lru_cap: DEFAULT_LRU_CAP)
|
|
13
|
+
super()
|
|
14
|
+
@entries = {}
|
|
15
|
+
@consumers = Hash.new { |hash, key| hash[key] = [] }
|
|
16
|
+
@scope_consumers = Hash.new { |hash, key| hash[key] = [] }
|
|
17
|
+
@lru_cap = lru_cap
|
|
18
|
+
# Tracks insertion/access order for time-travel cache entries only.
|
|
19
|
+
# Current-state entries (as_of: nil) live until explicit invalidation.
|
|
20
|
+
# Ruby Hash preserves insertion order; delete+reinsert = move to MRU.
|
|
21
|
+
@lru_order = {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def register_consumer(store, callable)
|
|
25
|
+
synchronize { @consumers[store] << callable }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def register_scope_consumer(store, scope, callable)
|
|
29
|
+
synchronize { @scope_consumers[[store, scope]] << callable }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get(store:, key:, as_of: nil, ttl: nil)
|
|
33
|
+
cache_key = [store, key, as_of]
|
|
34
|
+
entry = synchronize do
|
|
35
|
+
e = @entries[cache_key]
|
|
36
|
+
if e && as_of
|
|
37
|
+
@lru_order.delete(cache_key)
|
|
38
|
+
@lru_order[cache_key] = true
|
|
39
|
+
end
|
|
40
|
+
e
|
|
41
|
+
end
|
|
42
|
+
return nil unless entry
|
|
43
|
+
|
|
44
|
+
if ttl
|
|
45
|
+
age = Process.clock_gettime(Process::CLOCK_REALTIME) - entry.fetch(:cached_at)
|
|
46
|
+
return nil if age > ttl
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
entry.fetch(:fact)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def put(store:, key:, fact:, as_of: nil)
|
|
53
|
+
cache_key = [store, key, as_of]
|
|
54
|
+
synchronize do
|
|
55
|
+
@entries[cache_key] = {
|
|
56
|
+
fact: fact,
|
|
57
|
+
cached_at: Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
58
|
+
}
|
|
59
|
+
if as_of
|
|
60
|
+
@lru_order[cache_key] = true
|
|
61
|
+
evict_lru_if_needed
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def get_scope(store:, scope:, as_of: nil, ttl: nil)
|
|
67
|
+
cache_key = [:scope, store, scope, as_of]
|
|
68
|
+
entry = synchronize do
|
|
69
|
+
e = @entries[cache_key]
|
|
70
|
+
if e && as_of
|
|
71
|
+
@lru_order.delete(cache_key)
|
|
72
|
+
@lru_order[cache_key] = true
|
|
73
|
+
end
|
|
74
|
+
e
|
|
75
|
+
end
|
|
76
|
+
return nil unless entry
|
|
77
|
+
|
|
78
|
+
if ttl
|
|
79
|
+
age = Process.clock_gettime(Process::CLOCK_REALTIME) - entry.fetch(:cached_at)
|
|
80
|
+
return nil if age > ttl
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
entry.fetch(:facts)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def put_scope(store:, scope:, facts:, as_of: nil)
|
|
87
|
+
cache_key = [:scope, store, scope, as_of]
|
|
88
|
+
synchronize do
|
|
89
|
+
@entries[cache_key] = {
|
|
90
|
+
facts: facts,
|
|
91
|
+
cached_at: Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
92
|
+
}
|
|
93
|
+
if as_of
|
|
94
|
+
@lru_order[cache_key] = true
|
|
95
|
+
evict_lru_if_needed
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# +scope_changes+ is a Hash of { scope_name => :changed | :unchanged | :unknown }
|
|
101
|
+
# produced by IgniterStore#update_scope_indices. Scope consumers are only
|
|
102
|
+
# notified for scopes that are :changed or :unknown (conservative). Scopes
|
|
103
|
+
# marked :unchanged are skipped — their membership did not change and firing
|
|
104
|
+
# their consumers would be a false-positive thundering herd.
|
|
105
|
+
def invalidate(store:, key: nil, scope_changes: {})
|
|
106
|
+
point_targets, scope_notifications = synchronize do
|
|
107
|
+
affected_scopes = []
|
|
108
|
+
@entries.delete_if do |cache_key, _entry|
|
|
109
|
+
should_delete = if cache_key[0] == :scope && cache_key[1] == store
|
|
110
|
+
affected_scopes << cache_key[2]
|
|
111
|
+
true
|
|
112
|
+
else
|
|
113
|
+
cache_key[0] == store && (key.nil? || cache_key[1] == key)
|
|
114
|
+
end
|
|
115
|
+
@lru_order.delete(cache_key) if should_delete
|
|
116
|
+
should_delete
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
notify_scopes = affected_scopes.uniq.reject do |scope|
|
|
120
|
+
scope_changes[scope] == :unchanged
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
scope_notifs = notify_scopes.map do |scope|
|
|
124
|
+
[scope, @scope_consumers[[store, scope]].dup]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
[@consumers[store].dup, scope_notifs]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
point_targets.each { |t| notify(t, store, key) }
|
|
131
|
+
scope_notifications.each do |scope, targets|
|
|
132
|
+
targets.each { |t| notify_scope(t, store, scope) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def lru_size
|
|
137
|
+
synchronize { @lru_order.size }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def evict_lru_if_needed
|
|
143
|
+
while @lru_order.size > @lru_cap
|
|
144
|
+
oldest_key, = @lru_order.first
|
|
145
|
+
@lru_order.delete(oldest_key)
|
|
146
|
+
@entries.delete(oldest_key)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def notify(target, store, key)
|
|
151
|
+
target.call(store, key)
|
|
152
|
+
rescue StandardError
|
|
153
|
+
nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def notify_scope(target, store, scope)
|
|
157
|
+
target.call(store, scope)
|
|
158
|
+
rescue StandardError
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Store
|
|
5
|
+
class SchemaGraph
|
|
6
|
+
def initialize
|
|
7
|
+
@paths = Hash.new { |hash, key| hash[key] = [] }
|
|
8
|
+
@projections = {}
|
|
9
|
+
@derivations = []
|
|
10
|
+
@scatters = []
|
|
11
|
+
@relations = {}
|
|
12
|
+
@retention = {}
|
|
13
|
+
# Raw protocol descriptor storage (OP2 — metadata export)
|
|
14
|
+
@store_descriptors = {}
|
|
15
|
+
@history_descriptors = {}
|
|
16
|
+
@command_descriptors = {}
|
|
17
|
+
@effect_descriptors = {}
|
|
18
|
+
@subscription_descriptors = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def register(path)
|
|
22
|
+
@paths[path.store] << path
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def paths_for(store)
|
|
27
|
+
@paths[store].dup
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def consumers_for(store)
|
|
31
|
+
@paths[store].flat_map { |path| path.consumers.to_a }.uniq
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def path_for(store:, scope:)
|
|
35
|
+
@paths[store].find { |path| path.scope == scope }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def registered_stores
|
|
39
|
+
@paths.keys
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# --- Projection registry ---
|
|
43
|
+
|
|
44
|
+
def register_projection(projection_path)
|
|
45
|
+
@projections[projection_path.name] = projection_path
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def projection_for(name:)
|
|
50
|
+
@projections[name]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# All projections whose reads list includes the given store.
|
|
54
|
+
def projections_for_store(store:)
|
|
55
|
+
@projections.values.select { |p| p.reads.include?(store) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Compact snapshot of all registered projections, keyed by name.
|
|
59
|
+
# Parallel to metadata_snapshot for access paths.
|
|
60
|
+
def projection_snapshot
|
|
61
|
+
@projections.transform_values do |p|
|
|
62
|
+
{
|
|
63
|
+
name: p.name,
|
|
64
|
+
reads: p.reads,
|
|
65
|
+
relations: p.relations,
|
|
66
|
+
consumer_hint: p.consumer_hint,
|
|
67
|
+
reactive: p.reactive,
|
|
68
|
+
store_count: p.reads.size,
|
|
69
|
+
relation_count: p.relations.size
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# --- Derivation registry ---
|
|
75
|
+
|
|
76
|
+
def register_derivation(derivation_rule)
|
|
77
|
+
@derivations << derivation_rule
|
|
78
|
+
self
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# All derivation rules whose source_store matches the given store.
|
|
82
|
+
def derivations_for_store(store:)
|
|
83
|
+
@derivations.select { |r| r.source_store == store }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Compact snapshot of registered derivation rules (rule callables omitted).
|
|
87
|
+
def derivation_snapshot
|
|
88
|
+
@derivations.map.with_index do |r, i|
|
|
89
|
+
{
|
|
90
|
+
index: i,
|
|
91
|
+
source_store: r.source_store,
|
|
92
|
+
source_filters: r.source_filters,
|
|
93
|
+
target_store: r.target_store,
|
|
94
|
+
target_key: r.target_key.respond_to?(:call) ? :callable : r.target_key,
|
|
95
|
+
has_rule: true
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# --- Scatter Derivation registry ---
|
|
101
|
+
|
|
102
|
+
def register_scatter(scatter_rule)
|
|
103
|
+
@scatters << scatter_rule
|
|
104
|
+
self
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# All scatter rules whose source_store matches the given store.
|
|
108
|
+
def scatters_for_store(store:)
|
|
109
|
+
@scatters.select { |r| r.source_store == store }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Compact snapshot of registered scatter rules (rule callables omitted).
|
|
113
|
+
def scatter_snapshot
|
|
114
|
+
@scatters.map.with_index do |r, i|
|
|
115
|
+
{
|
|
116
|
+
index: i,
|
|
117
|
+
source_store: r.source_store,
|
|
118
|
+
partition_by: r.partition_by,
|
|
119
|
+
target_store: r.target_store,
|
|
120
|
+
has_rule: true
|
|
121
|
+
}
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --- Relation registry ---
|
|
126
|
+
|
|
127
|
+
def register_relation(relation_rule)
|
|
128
|
+
@relations[relation_rule.name] = relation_rule
|
|
129
|
+
self
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def relation_for(name:)
|
|
133
|
+
@relations[name]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def registered_relations
|
|
137
|
+
@relations.keys
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Compact snapshot of all registered relations (no callables — pure metadata).
|
|
141
|
+
def relation_snapshot
|
|
142
|
+
@relations.transform_values do |r|
|
|
143
|
+
{
|
|
144
|
+
name: r.name,
|
|
145
|
+
source: r.source,
|
|
146
|
+
partition: r.partition,
|
|
147
|
+
target: r.target,
|
|
148
|
+
index_store: :"__rel_#{r.name}"
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# --- Retention registry ---
|
|
154
|
+
|
|
155
|
+
def register_retention(store, policy)
|
|
156
|
+
@retention[store] = policy
|
|
157
|
+
self
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Returns the RetentionPolicy for store, or nil (meaning :permanent / no compaction).
|
|
161
|
+
def retention_for(store:)
|
|
162
|
+
@retention[store]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Stores with an explicitly registered retention policy (any strategy).
|
|
166
|
+
def retention_stores
|
|
167
|
+
@retention.keys
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Compact snapshot of all registered retention policies.
|
|
171
|
+
def retention_snapshot
|
|
172
|
+
@retention.transform_values { |p| { strategy: p.strategy, duration: p.duration } }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# --- Raw descriptor storage (OP2 — metadata export) ---
|
|
176
|
+
|
|
177
|
+
def register_store_descriptor(descriptor)
|
|
178
|
+
@store_descriptors[descriptor[:name].to_sym] = descriptor
|
|
179
|
+
self
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def register_history_descriptor(descriptor)
|
|
183
|
+
@history_descriptors[descriptor[:name].to_sym] = descriptor
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def register_subscription_descriptor(descriptor)
|
|
188
|
+
@subscription_descriptors[descriptor[:name].to_sym] = descriptor
|
|
189
|
+
self
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def register_command_descriptor(descriptor)
|
|
193
|
+
owner = descriptor[:owner].to_sym
|
|
194
|
+
name = descriptor[:name].to_sym
|
|
195
|
+
@command_descriptors[owner] ||= {}
|
|
196
|
+
@command_descriptors[owner][name] = descriptor
|
|
197
|
+
self
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def register_effect_descriptor(descriptor)
|
|
201
|
+
owner = descriptor[:owner].to_sym
|
|
202
|
+
name = descriptor[:name].to_sym
|
|
203
|
+
@effect_descriptors[owner] ||= {}
|
|
204
|
+
@effect_descriptors[owner][name] = descriptor
|
|
205
|
+
self
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def command_snapshot
|
|
209
|
+
@command_descriptors
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def effect_snapshot
|
|
213
|
+
@effect_descriptors
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Snapshot of all raw protocol-level descriptors registered via OP1.
|
|
217
|
+
def descriptor_snapshot
|
|
218
|
+
{
|
|
219
|
+
stores: @store_descriptors,
|
|
220
|
+
histories: @history_descriptors,
|
|
221
|
+
commands: @command_descriptors,
|
|
222
|
+
effects: @effect_descriptors,
|
|
223
|
+
subscriptions: @subscription_descriptors
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Returns a compact snapshot of all registered access paths keyed by store.
|
|
228
|
+
# Each entry describes how the engine routes scope queries for that store:
|
|
229
|
+
# scope name, lookup strategy, active filters, cache TTL, and consumer count.
|
|
230
|
+
# Index descriptors (which fields are co-indexed) remain manifest/facade
|
|
231
|
+
# metadata — they are a schema contract, not an engine routing concern.
|
|
232
|
+
def metadata_snapshot
|
|
233
|
+
@paths.each_with_object({}) do |(store, paths), snapshot|
|
|
234
|
+
snapshot[store] = paths.map do |path|
|
|
235
|
+
{
|
|
236
|
+
store: path.store,
|
|
237
|
+
scope: path.scope,
|
|
238
|
+
lookup: path.lookup,
|
|
239
|
+
filters: path.filters,
|
|
240
|
+
cache_ttl: path.cache_ttl,
|
|
241
|
+
consumer_count: path.consumers.to_a.size
|
|
242
|
+
}
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|