undertow 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/undertow/dsl.rb +17 -9
- data/lib/undertow/trackable.rb +85 -53
- data/lib/undertow/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4f841e4e433bebfa87d5086cdf621af5ede70a74754a98031645aa6181fcc8eb
|
|
4
|
+
data.tar.gz: b138e06bb8e54eaf5c40223fc07d08b6148bdbeefc7c9cfb8512b4b23c3a7d7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d31057a9a903b49a76208fbbeca609aa43ae43dcde2bd3dc98b142118f2694bc648cf1a4ee44958a7c52c1bbd9e3a85fc160c67105748096efadd6e7fb101d0
|
|
7
|
+
data.tar.gz: ef556acc038295b60fb3083fd3c79f70537f57d773f8ec10d0ead41b7c37019cdda3e20e035e2d0c407b1a5a4bc220562dcb4bd1beb471b596f9466777e2f8e4
|
data/lib/undertow/dsl.rb
CHANGED
|
@@ -5,14 +5,14 @@ module Undertow
|
|
|
5
5
|
# that calls these methods automatically registers itself with Undertow and
|
|
6
6
|
# gets Trackable behavior wired in at boot, no include needed.
|
|
7
7
|
#
|
|
8
|
-
# class
|
|
9
|
-
# undertow_on_drain ->(model_name, ids, deleted_ids) {
|
|
10
|
-
# undertow_skip %w[
|
|
8
|
+
# class Post < ApplicationRecord
|
|
9
|
+
# undertow_on_drain ->(model_name, ids, deleted_ids) { PostSyncJob.perform_later(ids, deleted_ids) }
|
|
10
|
+
# undertow_skip %w[view_count updated_at]
|
|
11
11
|
#
|
|
12
|
-
# undertow_depends_on :
|
|
13
|
-
# undertow_depends_on :
|
|
14
|
-
# resolver: ->(
|
|
15
|
-
# watched_columns: %w[
|
|
12
|
+
# undertow_depends_on :author, foreign_key: :author_id, watched_columns: %w[name bio]
|
|
13
|
+
# undertow_depends_on :tag,
|
|
14
|
+
# resolver: ->(tag) { Post.joins(:post_tags).where(post_tags: { tag_id: tag.id }) },
|
|
15
|
+
# watched_columns: %w[name slug]
|
|
16
16
|
# end
|
|
17
17
|
#
|
|
18
18
|
module DSL
|
|
@@ -21,19 +21,27 @@ module Undertow
|
|
|
21
21
|
_undertow_ensure_trackable!
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
# Suppress self-tracking when these root-model columns are the only changes.
|
|
25
|
+
# Accepts strings or symbols; blanks are ignored.
|
|
24
26
|
def undertow_skip(columns)
|
|
25
|
-
_undertow_config.skip_columns = columns
|
|
27
|
+
_undertow_config.skip_columns = Array(columns).map(&:to_s).reject(&:blank?).uniq
|
|
26
28
|
_undertow_ensure_trackable!
|
|
27
29
|
end
|
|
28
30
|
|
|
31
|
+
# Track invalidations from an upstream association.
|
|
32
|
+
# `watched_columns` accepts strings or symbols; blanks are ignored.
|
|
33
|
+
# Empty/nil means no watched-column filter.
|
|
29
34
|
def undertow_depends_on(association, foreign_key: nil, resolver: nil, watched_columns: nil)
|
|
30
35
|
raise ArgumentError, 'provide exactly one of foreign_key: or resolver:' unless foreign_key.nil? ^ resolver.nil?
|
|
31
36
|
|
|
37
|
+
normalized_watched = Array(watched_columns).map(&:to_s).
|
|
38
|
+
reject(&:blank?).uniq.presence
|
|
39
|
+
|
|
32
40
|
_undertow_config.dependencies << {
|
|
33
41
|
association: association,
|
|
34
42
|
foreign_key: foreign_key,
|
|
35
43
|
resolver: resolver,
|
|
36
|
-
watched_columns:
|
|
44
|
+
watched_columns: normalized_watched
|
|
37
45
|
}.freeze
|
|
38
46
|
_undertow_ensure_trackable!
|
|
39
47
|
end
|
data/lib/undertow/trackable.rb
CHANGED
|
@@ -4,8 +4,9 @@ module Undertow
|
|
|
4
4
|
# ActiveRecord concern mixed in automatically when a model uses the Undertow DSL
|
|
5
5
|
# (undertow_on_drain, undertow_skip, undertow_depends_on). Never included manually.
|
|
6
6
|
#
|
|
7
|
-
# Provides class-level callback registration and
|
|
8
|
-
# Callbacks are wired at boot by the Railtie
|
|
7
|
+
# Provides class-level callback registration and dependency push handlers, plus
|
|
8
|
+
# instance-level self-tracking handlers. Callbacks are wired at boot by the Railtie
|
|
9
|
+
# after all models are loaded.
|
|
9
10
|
module Trackable
|
|
10
11
|
extend ActiveSupport::Concern
|
|
11
12
|
|
|
@@ -30,86 +31,117 @@ module Undertow
|
|
|
30
31
|
(config.dependencies || []).each { |dep| _register_dep_callbacks!(dep) }
|
|
31
32
|
end
|
|
32
33
|
|
|
34
|
+
def _push_undertow_pending(ids)
|
|
35
|
+
Buffer.push_pending(name, ids)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def _push_undertow_deleted(ids)
|
|
39
|
+
Buffer.push_deleted(name, ids)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Enqueues records into the pending buffer without going through AR
|
|
43
|
+
# callbacks. Use this to replay a batch through the drain handler without
|
|
44
|
+
# triggering other model lifecycle callbacks, e.g. from a console or a
|
|
45
|
+
# data migration.
|
|
46
|
+
#
|
|
47
|
+
# Post.undertow_requeue(Post.where(author_id: 123))
|
|
48
|
+
# Post.undertow_requeue([136543, 136544])
|
|
49
|
+
# Post.undertow_requeue # all records
|
|
50
|
+
def undertow_requeue(scope_or_ids = :all)
|
|
51
|
+
ids = case scope_or_ids
|
|
52
|
+
when :all then pluck(:id)
|
|
53
|
+
when ActiveRecord::Relation then scope_or_ids.pluck(:id)
|
|
54
|
+
else Array(scope_or_ids)
|
|
55
|
+
end
|
|
56
|
+
_push_undertow_pending(ids)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def _push_dep_pending(record, dep)
|
|
60
|
+
ids = _dep_ids_for(record, dep)
|
|
61
|
+
_push_undertow_pending(ids) if ids.any?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def _push_dep_updated(record, dep)
|
|
65
|
+
watched = dep[:watched_columns]
|
|
66
|
+
# Two non-obvious cases where after_commit on :update fires:
|
|
67
|
+
# - no-op save: saved_changes is {}, suppressed when watched_columns is configured
|
|
68
|
+
# - touch (e.g. belongs_to touch: true): saved_changes contains only updated_at,
|
|
69
|
+
# suppressed only when watched_columns is configured and updated_at is not in it
|
|
70
|
+
return if watched && (record.saved_changes.keys & watched).none?
|
|
71
|
+
|
|
72
|
+
_push_dep_pending(record, dep)
|
|
73
|
+
end
|
|
74
|
+
|
|
33
75
|
private
|
|
34
76
|
|
|
35
77
|
def _register_self_callbacks!
|
|
36
|
-
after_commit :
|
|
78
|
+
after_commit :_push_self_created, on: :create
|
|
79
|
+
after_commit :_push_self_updated, on: :update
|
|
37
80
|
after_destroy :_push_self_deleted
|
|
38
|
-
after_restore :
|
|
81
|
+
after_restore :_push_self_restored if respond_to?(:after_restore)
|
|
39
82
|
end
|
|
40
83
|
|
|
41
84
|
def _register_dep_callbacks!(dep)
|
|
42
85
|
dep_class = _resolve_dep_class(dep)
|
|
43
|
-
return unless dep_class
|
|
44
|
-
|
|
45
86
|
root_class = self
|
|
46
|
-
watched = dep[:watched_columns].presence # [] treated same as nil, watch all
|
|
47
|
-
|
|
48
|
-
resolver = dep[:resolver] || begin
|
|
49
|
-
fk = dep[:foreign_key]
|
|
50
|
-
->(record) { root_class.where(fk => record.id) }
|
|
51
|
-
end
|
|
52
87
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
next unless ids.any?
|
|
88
|
+
dep_class.after_commit(on: :create) { root_class._push_dep_pending(self, dep) }
|
|
89
|
+
dep_class.after_commit(on: :update) { root_class._push_dep_updated(self, dep) }
|
|
56
90
|
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
# Soft-delete gems fire run_callbacks(:destroy), triggering after_destroy, but
|
|
92
|
+
# mark the record deleted via update_columns (bypassing after_commit),
|
|
93
|
+
# so the create/update callbacks above never double-fire on a soft delete.
|
|
94
|
+
dep_class.after_destroy { root_class._push_dep_pending(self, dep) }
|
|
59
95
|
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
dep_class.after_commit on: %i[create update] do
|
|
64
|
-
next if watched && (saved_changes.keys & watched).none?
|
|
65
|
-
|
|
66
|
-
push_pending.call(self)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Dep destroyed, reindex surviving root records. SoftDeletable calls
|
|
70
|
-
# run_callbacks(:destroy), which fires after_destroy, but update_columns does NOT
|
|
71
|
-
# trigger after_commit, so scoping after_commit to [:create, :update] above
|
|
72
|
-
# ensures destroy commits don't double-fire.
|
|
73
|
-
dep_class.after_destroy { push_pending.call(self) }
|
|
74
|
-
|
|
75
|
-
# Dep restored, after_restore is the only hook that fires because restore!
|
|
76
|
-
# uses update_columns, bypassing after_commit.
|
|
77
|
-
if dep_class.respond_to?(:after_restore)
|
|
78
|
-
dep_class.after_restore { push_pending.call(self) }
|
|
79
|
-
end
|
|
96
|
+
# Soft-delete restores use update_columns to unmark the record, bypassing
|
|
97
|
+
# after_commit. after_restore is the only hook that fires for restores.
|
|
98
|
+
dep_class.after_restore { root_class._push_dep_pending(self, dep) } if dep_class.respond_to?(:after_restore)
|
|
80
99
|
end
|
|
81
100
|
|
|
82
101
|
def _resolve_dep_class(dep)
|
|
83
|
-
|
|
102
|
+
reflect_on_association(dep[:association])&.klass ||
|
|
84
103
|
dep[:association].to_s.classify.constantize
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def _dep_ids_for(record, dep)
|
|
107
|
+
if dep[:resolver]
|
|
108
|
+
dep[:resolver].call(record).pluck(:id)
|
|
85
109
|
else
|
|
86
|
-
|
|
87
|
-
dep[:association].to_s.classify.constantize
|
|
110
|
+
where(dep[:foreign_key] => record.id).pluck(:id)
|
|
88
111
|
end
|
|
89
112
|
end
|
|
113
|
+
end
|
|
90
114
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _push_undertow_pending(ids)
|
|
94
|
-
Buffer.push_pending(name, ids)
|
|
95
|
-
end
|
|
115
|
+
private
|
|
96
116
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
end
|
|
117
|
+
def _push_self_created
|
|
118
|
+
_enqueue_self_pending
|
|
100
119
|
end
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
def _push_self_updated
|
|
122
|
+
return unless _self_update_requires_invalidation?
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
return if ignored.any? && saved_changes.any? && (saved_changes.keys - ignored).empty?
|
|
124
|
+
_enqueue_self_pending
|
|
125
|
+
end
|
|
107
126
|
|
|
108
|
-
|
|
127
|
+
def _push_self_restored
|
|
128
|
+
_enqueue_self_pending
|
|
109
129
|
end
|
|
110
130
|
|
|
111
131
|
def _push_self_deleted
|
|
112
132
|
self.class._push_undertow_deleted([id])
|
|
113
133
|
end
|
|
134
|
+
|
|
135
|
+
def _enqueue_self_pending
|
|
136
|
+
self.class._push_undertow_pending([id])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def _self_update_requires_invalidation?
|
|
140
|
+
changed = saved_changes.keys
|
|
141
|
+
return false if changed.empty?
|
|
142
|
+
|
|
143
|
+
ignored = self.class._undertow_ignored_columns
|
|
144
|
+
(changed - ignored).any?
|
|
145
|
+
end
|
|
114
146
|
end
|
|
115
147
|
end
|
data/lib/undertow/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: undertow
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nathan Allen
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|