upkeep-rails 0.1.6
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.
Potentially problematic release.
This version of upkeep-rails might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +614 -0
- data/docs/architecture/ambient-inputs-roadmap.md +308 -0
- data/docs/architecture/herb-roadmap.md +324 -0
- data/docs/architecture/identity-and-sharing.md +306 -0
- data/docs/architecture/query-dependencies.md +230 -0
- data/docs/architecture/subscription-store-contract.md +66 -0
- data/docs/cost-model-roadmap.md +704 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/handoff-2026-05-15.md +230 -0
- data/docs/production_roadmap.md +372 -0
- data/docs/shared-warm-scale-roadmap.md +214 -0
- data/docs/single-subscriber-cold-roadmap.md +192 -0
- data/docs/stress-test-findings.md +310 -0
- data/docs/testing.md +143 -0
- data/lib/generators/upkeep/install/install_generator.rb +127 -0
- data/lib/generators/upkeep/install/templates/create_upkeep_subscriptions.rb.erb +49 -0
- data/lib/generators/upkeep/install/templates/subscription.js +99 -0
- data/lib/generators/upkeep/install/templates/upkeep.rb +63 -0
- data/lib/upkeep/active_record_query.rb +294 -0
- data/lib/upkeep/capture/request.rb +150 -0
- data/lib/upkeep/dag/subscription_shape.rb +244 -0
- data/lib/upkeep/dag.rb +370 -0
- data/lib/upkeep/delivery/action_cable_adapter.rb +43 -0
- data/lib/upkeep/delivery/async_dispatcher.rb +102 -0
- data/lib/upkeep/delivery/broadcast_transport.rb +89 -0
- data/lib/upkeep/delivery/transport.rb +194 -0
- data/lib/upkeep/delivery/turbo_streams.rb +302 -0
- data/lib/upkeep/delivery.rb +7 -0
- data/lib/upkeep/dependencies.rb +518 -0
- data/lib/upkeep/herb/developer_report.rb +135 -0
- data/lib/upkeep/herb/manifest_cache.rb +83 -0
- data/lib/upkeep/herb/manifest_diff.rb +183 -0
- data/lib/upkeep/herb/source_instrumenter.rb +149 -0
- data/lib/upkeep/herb/template_manifest.rb +514 -0
- data/lib/upkeep/invalidation/collection_append.rb +84 -0
- data/lib/upkeep/invalidation/collection_member_replace.rb +78 -0
- data/lib/upkeep/invalidation/collection_prepend.rb +84 -0
- data/lib/upkeep/invalidation/collection_remove.rb +57 -0
- data/lib/upkeep/invalidation/planner.rb +360 -0
- data/lib/upkeep/invalidation.rb +7 -0
- data/lib/upkeep/rails/action_view_capture.rb +821 -0
- data/lib/upkeep/rails/activation_token.rb +55 -0
- data/lib/upkeep/rails/cable/channel.rb +143 -0
- data/lib/upkeep/rails/cable/subscriber_identity.rb +341 -0
- data/lib/upkeep/rails/cable.rb +4 -0
- data/lib/upkeep/rails/client_subscription.rb +45 -0
- data/lib/upkeep/rails/configuration.rb +245 -0
- data/lib/upkeep/rails/controller_runtime.rb +137 -0
- data/lib/upkeep/rails/delivery_job.rb +29 -0
- data/lib/upkeep/rails/install.rb +28 -0
- data/lib/upkeep/rails/railtie.rb +50 -0
- data/lib/upkeep/rails/replay.rb +176 -0
- data/lib/upkeep/rails/testing.rb +97 -0
- data/lib/upkeep/rails.rb +349 -0
- data/lib/upkeep/replay.rb +408 -0
- data/lib/upkeep/runtime.rb +1100 -0
- data/lib/upkeep/shared_streams.rb +72 -0
- data/lib/upkeep/subscriptions/active_record_store.rb +383 -0
- data/lib/upkeep/subscriptions/active_record_subscription_persistence.rb +407 -0
- data/lib/upkeep/subscriptions/active_registry.rb +87 -0
- data/lib/upkeep/subscriptions/async_durable_writer.rb +131 -0
- data/lib/upkeep/subscriptions/json_snapshot.rb +98 -0
- data/lib/upkeep/subscriptions/layered_reverse_index.rb +129 -0
- data/lib/upkeep/subscriptions/persistent_reverse_index.rb +223 -0
- data/lib/upkeep/subscriptions/registrar.rb +36 -0
- data/lib/upkeep/subscriptions/reverse_index.rb +298 -0
- data/lib/upkeep/subscriptions/shape.rb +116 -0
- data/lib/upkeep/subscriptions/store.rb +171 -0
- data/lib/upkeep/subscriptions.rb +7 -0
- data/lib/upkeep/targeting.rb +135 -0
- data/lib/upkeep/version.rb +5 -0
- data/lib/upkeep-rails.rb +3 -0
- data/lib/upkeep.rb +14 -0
- data/upkeep-rails.gemspec +54 -0
- metadata +320 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module Subscriptions
|
|
7
|
+
module JsonSnapshot
|
|
8
|
+
VERSION = 2
|
|
9
|
+
VERSION_KEY = "__upkeep_snapshot_version"
|
|
10
|
+
VALUE_KEY = "value"
|
|
11
|
+
SYMBOL_VALUE_KEY = "$sym"
|
|
12
|
+
SYMBOL_KEY_PREFIX = "$sym:"
|
|
13
|
+
STRING_KEY_PREFIX = "$str:"
|
|
14
|
+
JSON_KEY_PREFIX = "$json:"
|
|
15
|
+
RESERVED_STRING_KEYS = [SYMBOL_VALUE_KEY].freeze
|
|
16
|
+
RESERVED_STRING_KEY_PREFIXES = [SYMBOL_KEY_PREFIX, STRING_KEY_PREFIX, JSON_KEY_PREFIX].freeze
|
|
17
|
+
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
def dump(value)
|
|
21
|
+
{
|
|
22
|
+
VERSION_KEY => VERSION,
|
|
23
|
+
VALUE_KEY => encode(value)
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def load(snapshot)
|
|
28
|
+
snapshot = JSON.parse(snapshot) if snapshot.is_a?(String)
|
|
29
|
+
version = snapshot.fetch(VERSION_KEY)
|
|
30
|
+
unless version.to_i == VERSION
|
|
31
|
+
raise ArgumentError, "unsupported Upkeep JSON snapshot version: #{version.inspect}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
decode(snapshot.fetch(VALUE_KEY))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def encode(value)
|
|
38
|
+
case value
|
|
39
|
+
when Symbol
|
|
40
|
+
{ SYMBOL_VALUE_KEY => value.to_s }
|
|
41
|
+
when Hash
|
|
42
|
+
value.each_with_object({}) { |(key, nested_value), encoded| encoded[encode_key(key)] = encode(nested_value) }
|
|
43
|
+
when Array
|
|
44
|
+
value.map { |nested_value| encode(nested_value) }
|
|
45
|
+
when nil, true, false, Numeric, String
|
|
46
|
+
value
|
|
47
|
+
else
|
|
48
|
+
raise TypeError, "cannot persist #{value.class.name} in an Upkeep JSON snapshot"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def decode(value)
|
|
53
|
+
case value
|
|
54
|
+
when Hash
|
|
55
|
+
return value.fetch(SYMBOL_VALUE_KEY).to_sym if value.size == 1 && value.key?(SYMBOL_VALUE_KEY)
|
|
56
|
+
|
|
57
|
+
value.each_with_object({}) do |(key, nested_value), decoded|
|
|
58
|
+
decoded[decode_key(key)] = decode(nested_value)
|
|
59
|
+
end
|
|
60
|
+
when Array
|
|
61
|
+
value.map { |nested_value| decode(nested_value) }
|
|
62
|
+
else
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def encode_key(key)
|
|
68
|
+
case key
|
|
69
|
+
when Symbol
|
|
70
|
+
"#{SYMBOL_KEY_PREFIX}#{key}"
|
|
71
|
+
when String
|
|
72
|
+
reserved_string_key?(key) ? "#{STRING_KEY_PREFIX}#{key}" : key
|
|
73
|
+
when nil, true, false, Numeric
|
|
74
|
+
"#{JSON_KEY_PREFIX}#{JSON.generate(encode(key))}"
|
|
75
|
+
else
|
|
76
|
+
raise TypeError, "cannot persist #{key.class.name} as an Upkeep JSON snapshot key"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def decode_key(key)
|
|
81
|
+
if key.start_with?(SYMBOL_KEY_PREFIX)
|
|
82
|
+
key.delete_prefix(SYMBOL_KEY_PREFIX).to_sym
|
|
83
|
+
elsif key.start_with?(STRING_KEY_PREFIX)
|
|
84
|
+
key.delete_prefix(STRING_KEY_PREFIX)
|
|
85
|
+
elsif key.start_with?(JSON_KEY_PREFIX)
|
|
86
|
+
decode(JSON.parse(key.delete_prefix(JSON_KEY_PREFIX)))
|
|
87
|
+
else
|
|
88
|
+
key
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def reserved_string_key?(key)
|
|
93
|
+
RESERVED_STRING_KEYS.include?(key) || RESERVED_STRING_KEY_PREFIXES.any? { |prefix| key.start_with?(prefix) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module Subscriptions
|
|
7
|
+
class LayeredReverseIndex
|
|
8
|
+
LOOKUP_NOTIFICATION = "lookup_subscription_index.upkeep"
|
|
9
|
+
|
|
10
|
+
def initialize(active_index:, persistent_index:, persistent_count:, store:, pending_index: nil)
|
|
11
|
+
@active_index = active_index
|
|
12
|
+
@persistent_index = persistent_index
|
|
13
|
+
@persistent_count = persistent_count
|
|
14
|
+
@store = store
|
|
15
|
+
@pending_index = pending_index
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def entries_for(changes)
|
|
19
|
+
if ActiveSupport::Notifications.notifier.listening?(LOOKUP_NOTIFICATION)
|
|
20
|
+
payload = { changes: Array(changes).size, store: store }
|
|
21
|
+
ActiveSupport::Notifications.instrument(LOOKUP_NOTIFICATION, payload) do
|
|
22
|
+
entries_for_with_payload(changes, payload)
|
|
23
|
+
end
|
|
24
|
+
else
|
|
25
|
+
entries_for_without_payload(changes)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def entries_for_without_payload(changes)
|
|
30
|
+
active_entries = active_index.entries_for(changes)
|
|
31
|
+
return persistent_index.entries_for(changes) if active_index.count.zero?
|
|
32
|
+
return active_entries if active_index.covers?(persistent_subscription_count)
|
|
33
|
+
|
|
34
|
+
merge_entries(active_entries, persistent_index.entries_for(changes))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def entries_for_with_payload(changes, payload)
|
|
38
|
+
persistent_summary = persistent_index.summary
|
|
39
|
+
active_entries = active_index.entries_for(changes)
|
|
40
|
+
active_count = active_index.count
|
|
41
|
+
pending_entries = pending_entries_for(changes)
|
|
42
|
+
pending_count = pending_count_for_payload
|
|
43
|
+
payload[:active_entries] = active_entries.size
|
|
44
|
+
payload[:active_subscriptions] = active_count
|
|
45
|
+
payload[:pending_entries] = pending_entries.size
|
|
46
|
+
payload[:pending_subscriptions] = pending_count
|
|
47
|
+
payload[:persistent_direct_index_entries] = persistent_summary.fetch(:direct).fetch(:entries)
|
|
48
|
+
payload[:persistent_shape_index_entries] = persistent_summary.fetch(:shape).fetch(:entries)
|
|
49
|
+
payload[:persistent_direct_lookup_keys] = persistent_summary.fetch(:direct).fetch(:lookup_keys)
|
|
50
|
+
payload[:persistent_shape_lookup_keys] = persistent_summary.fetch(:shape).fetch(:lookup_keys)
|
|
51
|
+
payload[:persistent_shape_keys] = persistent_summary.fetch(:shape).fetch(:shape_keys)
|
|
52
|
+
payload[:persistent_shape_subscriptions] = persistent_summary.fetch(:shape).fetch(:subscriptions)
|
|
53
|
+
|
|
54
|
+
if active_count.zero?
|
|
55
|
+
persistent_entries = persistent_index.entries_for(changes)
|
|
56
|
+
payload[:mode] = persistent_entries.empty? && pending_entries.any? ? "pending_activation" : "persistent"
|
|
57
|
+
payload[:persistent_entries] = persistent_entries.size
|
|
58
|
+
apply_miss_reason(payload, active_entries: active_entries, persistent_entries: persistent_entries, pending_entries: pending_entries)
|
|
59
|
+
return persistent_entries
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if active_index.covers?(persistent_subscription_count)
|
|
63
|
+
payload[:mode] = "active"
|
|
64
|
+
payload[:persistent_entries] = 0
|
|
65
|
+
apply_miss_reason(payload, active_entries: active_entries, persistent_entries: [], pending_entries: pending_entries)
|
|
66
|
+
return active_entries
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
persistent_entries = persistent_index.entries_for(changes)
|
|
70
|
+
payload[:mode] = "active_and_persistent"
|
|
71
|
+
payload[:persistent_entries] = persistent_entries.size
|
|
72
|
+
apply_miss_reason(payload, active_entries: active_entries, persistent_entries: persistent_entries, pending_entries: pending_entries)
|
|
73
|
+
merge_entries(active_entries, persistent_entries)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def summary
|
|
77
|
+
persistent = persistent_index.summary
|
|
78
|
+
active = active_index.summary
|
|
79
|
+
pending = pending_index&.summary || { lookup_keys: 0, entries: 0, subscriptions: 0 }
|
|
80
|
+
mode = active_index.covers?(persistent_subscription_count) ? :active : :active_and_persistent
|
|
81
|
+
totals = if mode == :active
|
|
82
|
+
active
|
|
83
|
+
else
|
|
84
|
+
{
|
|
85
|
+
lookup_keys: active.fetch(:lookup_keys) + persistent.fetch(:lookup_keys),
|
|
86
|
+
entries: active.fetch(:entries) + persistent.fetch(:entries)
|
|
87
|
+
}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
{
|
|
91
|
+
lookup_keys: totals.fetch(:lookup_keys),
|
|
92
|
+
entries: totals.fetch(:entries),
|
|
93
|
+
mode: mode,
|
|
94
|
+
active: active,
|
|
95
|
+
pending: pending,
|
|
96
|
+
persistent: persistent
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
attr_reader :active_index, :persistent_index, :persistent_count, :store, :pending_index
|
|
103
|
+
|
|
104
|
+
def persistent_subscription_count
|
|
105
|
+
persistent_count.call
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def merge_entries(active_entries, persistent_entries)
|
|
109
|
+
(active_entries + persistent_entries).uniq do |entry|
|
|
110
|
+
[entry.subscription_id, entry.owner_id, entry.dependency_cache_key]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def pending_entries_for(changes)
|
|
115
|
+
pending_index ? pending_index.entries_for(changes) : []
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def pending_count_for_payload
|
|
119
|
+
pending_index&.count || 0
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def apply_miss_reason(payload, active_entries:, persistent_entries:, pending_entries:)
|
|
123
|
+
return if active_entries.any? || persistent_entries.any?
|
|
124
|
+
|
|
125
|
+
payload[:miss_reason] = pending_entries.any? ? "not_activated_yet" : "no_matching_subscriber"
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "../dependencies"
|
|
6
|
+
require_relative "json_snapshot"
|
|
7
|
+
require_relative "reverse_index"
|
|
8
|
+
|
|
9
|
+
module Upkeep
|
|
10
|
+
module Subscriptions
|
|
11
|
+
class PersistentReverseIndex
|
|
12
|
+
LOOKUP_COLUMNS = [
|
|
13
|
+
:subscription_id,
|
|
14
|
+
:lookup_key_digest,
|
|
15
|
+
:dependency_source,
|
|
16
|
+
:lookup_table,
|
|
17
|
+
:lookup_record_id_snapshot,
|
|
18
|
+
:lookup_attribute,
|
|
19
|
+
:dependency_table,
|
|
20
|
+
:dependency_predicate_digest,
|
|
21
|
+
:dependency_metadata_snapshot,
|
|
22
|
+
:owner_ids_snapshot
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
SHAPE_LOOKUP_COLUMNS = [
|
|
26
|
+
:subscription_shape_key,
|
|
27
|
+
:lookup_key_digest,
|
|
28
|
+
:dependency_source,
|
|
29
|
+
:lookup_table,
|
|
30
|
+
:lookup_record_id_snapshot,
|
|
31
|
+
:lookup_attribute,
|
|
32
|
+
:dependency_table,
|
|
33
|
+
:dependency_predicate_digest,
|
|
34
|
+
:dependency_metadata_snapshot,
|
|
35
|
+
:owner_ids_snapshot
|
|
36
|
+
].freeze
|
|
37
|
+
|
|
38
|
+
def initialize(reverse_index:, index_record:, shape_index_record:, subscription_record:)
|
|
39
|
+
@reverse_index = reverse_index
|
|
40
|
+
@index_record = index_record
|
|
41
|
+
@shape_index_record = shape_index_record
|
|
42
|
+
@subscription_record = subscription_record
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def entries_for(changes)
|
|
46
|
+
persistent_entries_for(changes)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def summary
|
|
50
|
+
direct_lookup_key_digests = index_record.distinct.pluck(:lookup_key_digest)
|
|
51
|
+
shape_lookup_key_digests = shape_index_record.distinct.pluck(:lookup_key_digest)
|
|
52
|
+
direct_entries = index_record.count
|
|
53
|
+
shape_entries = shape_index_record.count
|
|
54
|
+
{
|
|
55
|
+
lookup_keys: (direct_lookup_key_digests + shape_lookup_key_digests).uniq.size,
|
|
56
|
+
entries: direct_entries + shape_entries,
|
|
57
|
+
direct: {
|
|
58
|
+
lookup_keys: direct_lookup_key_digests.uniq.size,
|
|
59
|
+
entries: direct_entries
|
|
60
|
+
},
|
|
61
|
+
shape: {
|
|
62
|
+
lookup_keys: shape_lookup_key_digests.uniq.size,
|
|
63
|
+
entries: shape_entries,
|
|
64
|
+
shape_keys: shape_index_record.distinct.count(:subscription_shape_key),
|
|
65
|
+
subscriptions: subscription_record.where.not(subscription_shape_key: nil).count
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.digest(value)
|
|
71
|
+
Digest::SHA256.hexdigest(JSON.generate(canonical_lookup_value(value)))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.canonical_lookup_value(value)
|
|
75
|
+
case value
|
|
76
|
+
when Array
|
|
77
|
+
value.map { |item| canonical_lookup_value(item) }
|
|
78
|
+
when Hash
|
|
79
|
+
value.keys.sort_by(&:to_s).map do |key|
|
|
80
|
+
[canonical_lookup_value(key), canonical_lookup_value(value.fetch(key))]
|
|
81
|
+
end
|
|
82
|
+
when Symbol
|
|
83
|
+
["symbol", value.to_s]
|
|
84
|
+
when String
|
|
85
|
+
["string", value.encode(Encoding::UTF_8)]
|
|
86
|
+
else
|
|
87
|
+
value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
attr_reader :reverse_index, :index_record, :shape_index_record, :subscription_record
|
|
94
|
+
|
|
95
|
+
def persistent_entries_for(changes)
|
|
96
|
+
lookup_keys = Array(changes).flat_map { |change| reverse_index.lookup_keys_for_change(change) }.uniq
|
|
97
|
+
lookup_keys_by_digest = Hash.new { |hash, digest| hash[digest] = [] }
|
|
98
|
+
lookup_keys.each do |lookup_key|
|
|
99
|
+
lookup_keys_by_digest[self.class.digest(lookup_key)] << lookup_key
|
|
100
|
+
end
|
|
101
|
+
lookup_key_digests = lookup_keys_by_digest.keys
|
|
102
|
+
|
|
103
|
+
direct_entries = index_record
|
|
104
|
+
.where(lookup_key_digest: lookup_key_digests)
|
|
105
|
+
.pluck(*LOOKUP_COLUMNS)
|
|
106
|
+
.flat_map { |row| entries_for_row(row, lookup_keys_by_digest) }
|
|
107
|
+
|
|
108
|
+
shape_entries = shape_entries_for(lookup_key_digests, lookup_keys_by_digest)
|
|
109
|
+
|
|
110
|
+
(direct_entries + shape_entries).uniq { |entry| [entry.subscription_id, entry.owner_id, entry.dependency_cache_key] }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def shape_entries_for(lookup_key_digests, lookup_keys_by_digest)
|
|
114
|
+
shape_rows = shape_index_record
|
|
115
|
+
.where(lookup_key_digest: lookup_key_digests)
|
|
116
|
+
.pluck(*SHAPE_LOOKUP_COLUMNS)
|
|
117
|
+
return [] if shape_rows.empty?
|
|
118
|
+
|
|
119
|
+
subscription_ids_by_shape_key = subscription_ids_by_shape_key(
|
|
120
|
+
shape_rows.map { |row| row.fetch(0) }.uniq
|
|
121
|
+
)
|
|
122
|
+
shape_rows.flat_map do |row|
|
|
123
|
+
entries_for_shape_row(row, lookup_keys_by_digest, subscription_ids_by_shape_key)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def entries_for_row(row, lookup_keys_by_digest)
|
|
128
|
+
attributes = LOOKUP_COLUMNS.zip(row).to_h
|
|
129
|
+
lookup_keys = lookup_keys_by_digest.fetch(attributes.fetch(:lookup_key_digest)) { return [] }
|
|
130
|
+
return [] unless lookup_keys.any? { |lookup_key| lookup_key_matches_row?(lookup_key, attributes) }
|
|
131
|
+
|
|
132
|
+
dependency = dependency_for_row(attributes)
|
|
133
|
+
dependency_cache_key = dependency.cache_key
|
|
134
|
+
JsonSnapshot.load(attributes.fetch(:owner_ids_snapshot)).map do |owner_id|
|
|
135
|
+
ReverseIndex::Entry.new(
|
|
136
|
+
attributes.fetch(:subscription_id),
|
|
137
|
+
owner_id,
|
|
138
|
+
dependency_cache_key,
|
|
139
|
+
dependency,
|
|
140
|
+
nil,
|
|
141
|
+
nil
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def entries_for_shape_row(row, lookup_keys_by_digest, subscription_ids_by_shape_key)
|
|
147
|
+
attributes = SHAPE_LOOKUP_COLUMNS.zip(row).to_h
|
|
148
|
+
lookup_keys = lookup_keys_by_digest.fetch(attributes.fetch(:lookup_key_digest)) { return [] }
|
|
149
|
+
return [] unless lookup_keys.any? { |lookup_key| lookup_key_matches_row?(lookup_key, attributes) }
|
|
150
|
+
|
|
151
|
+
dependency = dependency_for_row(attributes)
|
|
152
|
+
dependency_cache_key = dependency.cache_key
|
|
153
|
+
subscription_ids = subscription_ids_by_shape_key.fetch(attributes.fetch(:subscription_shape_key), [])
|
|
154
|
+
subscription_ids.flat_map do |subscription_id|
|
|
155
|
+
JsonSnapshot.load(attributes.fetch(:owner_ids_snapshot)).map do |owner_id|
|
|
156
|
+
ReverseIndex::Entry.new(
|
|
157
|
+
subscription_id,
|
|
158
|
+
owner_id,
|
|
159
|
+
dependency_cache_key,
|
|
160
|
+
dependency,
|
|
161
|
+
nil,
|
|
162
|
+
nil
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def lookup_key_matches_row?(lookup_key, attributes)
|
|
169
|
+
case lookup_key.fetch(0)
|
|
170
|
+
when :active_record_attribute
|
|
171
|
+
lookup_key.fetch(1).to_s == attributes.fetch(:lookup_table).to_s &&
|
|
172
|
+
lookup_key.fetch(2) == JsonSnapshot.load(attributes.fetch(:lookup_record_id_snapshot)) &&
|
|
173
|
+
lookup_key.fetch(3).to_s == attributes.fetch(:lookup_attribute).to_s
|
|
174
|
+
when :active_record_attribute_any_id
|
|
175
|
+
lookup_key.fetch(1).to_s == attributes.fetch(:lookup_table).to_s &&
|
|
176
|
+
attributes.fetch(:lookup_record_id_snapshot).nil? &&
|
|
177
|
+
lookup_key.fetch(2).to_s == attributes.fetch(:lookup_attribute).to_s
|
|
178
|
+
when :active_record_collection_column
|
|
179
|
+
lookup_key.fetch(1).to_s == attributes.fetch(:lookup_table).to_s &&
|
|
180
|
+
attributes.fetch(:lookup_record_id_snapshot).nil? &&
|
|
181
|
+
lookup_key.fetch(2).to_s == attributes.fetch(:lookup_attribute).to_s
|
|
182
|
+
else
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def dependency_for_row(attributes)
|
|
188
|
+
source = attributes.fetch(:dependency_source).to_sym
|
|
189
|
+
case source
|
|
190
|
+
when :active_record_attribute
|
|
191
|
+
Dependencies::ActiveRecordAttribute.new(
|
|
192
|
+
table: attributes.fetch(:dependency_table),
|
|
193
|
+
id: attributes[:lookup_record_id_snapshot] && JsonSnapshot.load(attributes.fetch(:lookup_record_id_snapshot)),
|
|
194
|
+
attribute: attributes.fetch(:lookup_attribute)
|
|
195
|
+
)
|
|
196
|
+
when :active_record_collection, :active_record_query
|
|
197
|
+
metadata = JsonSnapshot.load(attributes.fetch(:dependency_metadata_snapshot))
|
|
198
|
+
dependency_class = source == :active_record_query ? Dependencies::ActiveRecordQuery : Dependencies::ActiveRecordCollection
|
|
199
|
+
dependency_class.new(
|
|
200
|
+
primary_table: attributes.fetch(:dependency_table),
|
|
201
|
+
table_columns: metadata.fetch(:table_columns),
|
|
202
|
+
coverage: metadata.fetch(:coverage),
|
|
203
|
+
sql: metadata.fetch(:sql),
|
|
204
|
+
predicates: metadata.fetch(:predicates)
|
|
205
|
+
)
|
|
206
|
+
else
|
|
207
|
+
raise ArgumentError, "unsupported persistent dependency source: #{source.inspect}"
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def subscription_ids_by_shape_key(shape_keys)
|
|
212
|
+
shape_key_lookup = shape_keys.to_h { |shape_key| [shape_key, []] }
|
|
213
|
+
subscription_record
|
|
214
|
+
.where(subscription_shape_key: shape_keys)
|
|
215
|
+
.pluck(:subscription_shape_key, :id)
|
|
216
|
+
.each do |shape_key, id|
|
|
217
|
+
shape_key_lookup[shape_key] << id if shape_key_lookup.key?(shape_key)
|
|
218
|
+
end
|
|
219
|
+
shape_key_lookup.transform_values { |ids| ids.sort_by(&:to_s) }
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shape"
|
|
4
|
+
|
|
5
|
+
module Upkeep
|
|
6
|
+
module Subscriptions
|
|
7
|
+
class Registrar
|
|
8
|
+
Registration = Data.define(:identity, :decision, :subscription, :shape)
|
|
9
|
+
|
|
10
|
+
def initialize(store:, shape_cache: ShapeCache.new)
|
|
11
|
+
@store = store
|
|
12
|
+
@shape_cache = shape_cache
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register(identity:, decision:, recorder:, metadata: {}, signature: nil)
|
|
16
|
+
shape = shape_cache.resolve(recorder: recorder, decision: decision, signature: signature)
|
|
17
|
+
subscription = store.register(
|
|
18
|
+
subscriber_id: identity.subscriber_id,
|
|
19
|
+
recorder: recorder,
|
|
20
|
+
metadata: metadata.merge(
|
|
21
|
+
shared_stream_names: shape.shared_stream_names,
|
|
22
|
+
subscription_shape_key: shape.key,
|
|
23
|
+
subscription_shape_cache: shape.cache_state
|
|
24
|
+
).compact,
|
|
25
|
+
entries: shape.entries
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
Registration.new(identity, decision, subscription, shape)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :store, :shape_cache
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|