fragmentary 0.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0005ce07a2de70b95fc9b105b3ab12a7d763fb2ededa0423845bbfc3c0b45e8d
4
- data.tar.gz: e2202f4d38a1189ba956e7b88150178e962581ed93e00f3d74fb8144b193055e
3
+ metadata.gz: 61a94b394035bb4b3ec317316e46fa77ca925cc15a162a76d6401500e6636b5d
4
+ data.tar.gz: 8db2f91e9a9b23f3ccdafbc2f800695e653426a656887959f807c80eca10c901
5
5
  SHA512:
6
- metadata.gz: 1665f0be866246efc7a1ba157d1216f63a39162261a680e6e8542884461c2285137ce350efdd4ed233ee6cb4b88484664ab5b7436a0f84e5fbb80c892ccab7ae
7
- data.tar.gz: 0e4d6a8bd99ae54ff6c3715e203cde760382ef9c48a5ee241ae645ce58dd07dcb1d17b819f10771edcf5d5d113016d0d115fd5a08b1218e3060fef0bf57553de
6
+ metadata.gz: bc4f5da40872cfb5327af28cb81862e61ba201258bc8338fa6ad9871439dac0133af4c102c10b09cd0ed25acb883c53d428ba3632085ac4b67cb7f662ccf6c16
7
+ data.tar.gz: '0628b394849e14ed4fed5ebe18e7b59a4badfe83d16b63f24965e6e6a2a4d656aec57f627000d70286c79152d2d028fb231c0b7c89a2eabfffb455140f7404e6'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ### 0.4.0
2
+ - Uses ActiveJob as the interface for asynchronous processes instead of using Delayed::Job directly.
3
+ - Updates Rails dependency to ~>6.0 as earlier versions lack support for the custom job serialization needed to use ActiveJob. Accordingly, Rails 5.x and earlier are no longer supported.
4
+ - Adds a convenience method RequestQueue.send_all for sending internal application requests.
5
+ - Inserts optional HTML comments containing time and version stamps into fragment content.
6
+ - Adds utility methods for interacting with the cache_store from the console.
7
+ - Updates documentation for new features and to reflect Rails use of cache versioning.
8
+
1
9
  ### 0.3.0
2
10
  - Updates gem to support Rails 5.x (Rails 4.x and earlier are no longer supported due to Rails API changes).
3
11
  - Adds support for multiple application instances, allowing pre-release application code to be staged for testing.
data/README.md CHANGED
@@ -10,12 +10,16 @@ Fragmentary augments the fragment caching capabilities of Ruby on Rails to suppo
10
10
  Fragmentary has been extracted from [Persuasive Thinking](http://persuasivethinking.com) where it is currently in active use.
11
11
 
12
12
  ## Background
13
- In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. The content is stored in the cache with a key value derived from the`id` and `updated_at` attributes of that record. If any attributes of the record change, the cached entry automatically expires and on the next browser request for that content the fragment is re-rendered using the current data. In the view, the `cache` helper is used to specify the record used to determine the key and define the content to be rendered within the fragment, e.g.:
13
+ In simple cases, Rails' native support for fragment caching assumes that a fragment's content is a representation of a specific application data record. In the view, the `cache` helper is used to specify the record and to define the content to be rendered within the fragment, e.g.:
14
14
  ```
15
15
  <% cache product do %>
16
16
  <%= render product %>
17
17
  <% end %>
18
18
  ```
19
+ The content is stored in the cache using a _key_ value derived from the record's `id` attribute. The key identifies where the cached content is stored. In addition, a _version_ value derived from the record's `updated_at` attribute is stored along with the content [^1]. If any attributes of the record change, the `updated_at` attribute also changes; the next time a browser requests the content, a mismatch will exist between the calculated and stored values of the version, causing the cache to expire and the fragment to be re-rendered using the current data.
20
+
21
+ [^1]: Prior to Rails 5.2, the value of `updated_at` was incorporated into the cache key rather that the version. That behavior is still available by setting `config.active_record.cache_versioning = false` in `config/application.rb`.
22
+
19
23
  For data models with a `has_one` or `has_many` association, nested (Russian Doll) caching is possible with the inner fragment representing the associated record. For example if a product has many games -
20
24
  ```
21
25
  <% cache product do %>
@@ -35,14 +39,14 @@ end
35
39
 
36
40
  Part of the beauty of this approach is the fact that any change in data requiring the cache to be refreshed is detected by inspecting just a single `updated_at` attribute of each record. However it is less effective at handling cases where the content of a fragment depends in more complex ways on multiple records from multiple models. The fact that automatic updating of nested fragments is limited to `belongs_to` associations is one aspect of this.
37
41
 
38
- It is certainly possible to construct more complex keys from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
42
+ It is certainly possible to construct more complex keys and/or versions from multiple records and models. However this requires retrieving all of those records from the database and computing keys from them for all fragments contained in the server's response every time a request is received, potentially undermining the benefit caching is intended to provide. A related challenge exists in dealing with user-specific content. Here again the user record can easily be incorporated into the key. However if the content of a fragment really must be customized to individual users, this can lead to the number of cache entries escalating dramatically.
39
43
 
40
- A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, but we are still left with the challenge of how to implement nesting in the case of more complex data associations.
44
+ A further limitation is that the rendering and storage of cache content relies on an explicit request being received from a user's browser, meaning that at least one user experiences a response time that doesn't benefit from caching every time a relevant change in data occurs. Nested fragments can mitigate this problem for pre-existing pages, since only part of the response need be re-built, though we are still left with the challenge of how to implement nesting in the case of more complex data associations.
41
45
 
42
46
  ## Fragmentary - General Approach
43
- Fragmentary uses a database table and corresponding ActiveRecord model that are separate from your application data specifically to represent view fragments. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the cache key is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
47
+ In Fragmentary, view fragments are associated with records from an ActiveRecord model and corresponding database table that are _separate_ from your application data. Records in this table serve only as metadata, recording the type of content each fragment contains, where it is located, and when it was last updated. These records play the same role with respect to caching as an application data record in Rails' native approach, i.e. internally a fragment record is passed to Rails' `cache` method in the same way that `product` was in the earlier example, and the version of a cache entry is derived from the fragment record's `updated_at` attribute. A publish-subscribe mechanism is used to automatically update this timestamp whenever any application data affecting the content of a fragment is added, modified or destroyed, causing the fragment's cached content to be expired.
44
48
 
45
- To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that the whole page (or part thereof in the case of some AJAX requests) is refreshed.
49
+ To support fragment nesting, each fragment record also includes a `parent_id` attribute pointing to its immediate parent, or containing, fragment. Whenever the `updated_at` attribute on an inner fragment changes (initially as a result of a change in some application data the fragment is associated with), as well as expiring the cached content for that fragment, the parent fragment is automatically touched, thus expiring the containing cache as well. This process continues up through all successive ancestors, ensuring that all of the cached content containing the change is refreshed.
46
50
 
47
51
  Rather than use Rails' native `cache` helper directly, Fragmentary provides some new helper methods that hide some of the necessary internals involved in working with an explicit fragment model. Additional features include the ability to cache separate versions of fragments for different types of users and the ability to insert user-specific content after cached content is retrieved from the cache store. Fragmentary also supports automatic background updating of cached content within seconds of application data changing, avoiding the need for a user to visit a page in order to update the cache.
48
52
 
@@ -117,15 +121,30 @@ class ProductTemplate < Fragment
117
121
  needs_record_id :type => 'Product'
118
122
  end
119
123
  ```
120
- Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record. If you need to define further subclasses of your initial subclass, you can if necessary declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
124
+ Here `needs_record_id` indicates that there is a separate `ProductTemplate` fragment associated with each `Product` record and the `record_id` attribute of a `ProductTemplate` record contains the `id` of that product.
125
+
126
+ If you need to define further subclasses of your initial subclass, you can declare `needs_record_id` on the latter without providing a type and specify the type separately on the individual subclasses using:
121
127
 
122
128
  `set_record_type 'SomeModelName'`
123
129
 
124
- We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics but are used in different contexts to represent different kinds of content.
130
+ We've used this, for example, for fragment types representing different kinds of list items that have certain generic characteristics in common but are used in different contexts to represent content with different sub-class-specific dependencies on application data.
131
+
132
+ Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block to define methods that handle create, update and destroy events on your application data, typically to touch the fragment records affected by a change in the application data. For example, if product listings include the names of all the categories the products belong to, and those categories are represented by a separate ActiveRecord model, if the wording of a category name changes, you could handle that as follows:
133
+ ```
134
+ class ProductTemplate < Fragment
135
+ needs_record_id :type => 'Product'
125
136
 
126
- Within the body of the fragment subclass definition, for each application model whose records the content of the fragment depends upon, use the `subscribe_to` method with a block containing method definitions to handle create, update and destroy events on your application data, typically to touch the fragment records affected by the application data change. The names of these methods follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
137
+ subscribe_to 'ProductCategory' do
138
+ def update_product_category_successful(product_category)
139
+ # touch ProductTemplate records whose record_id matches the ids of products within the specified product_category
140
+ ProductTemplate.where(:record_id => product_category.products.map(&:id)).each(&:touch)
141
+ end
142
+ end
143
+ end
144
+ ```
145
+ The effect of this will be to expire the product template fragment for every product contained within the affected category. `subscribe_to` takes one parameter, the name of the application data class being handled, while the names of the methods defined within the block follow the form used in the wisper-activerecord gem, i.e. `create_<model_name>_successful`, `update_<model_name>_successful` and `destroy_<model_name>_successful`, each taking a single argument representing the application data record that has changed.
127
146
 
128
- Within the body of each method you define within the `subscribe_to` block, you can retrieve and touch the fragment records affected by the change in application data. The method `touch_fragments_for_record` can be used for convenience. That method takes an individual application data record or record_id or an array of either. So, for example, if product listings include the names of all the categories the products belong to, with those categories being represented by a separate ActiveRecord model, and the wording of a category name changes, you could handle that as follows.
147
+ Within the body of each method that you define within the block, the convenience method `touch_fragments_for_record` can be used retrieve and touch the fragment records affected by the change in application data. This method takes an individual application data record or a record_id or an array of either. So the example above can be written instead as:
129
148
  ```
130
149
  class ProductTemplate < Fragment
131
150
  needs_record_id :type => 'Product'
@@ -137,9 +156,7 @@ class ProductTemplate < Fragment
137
156
  end
138
157
  end
139
158
  ```
140
- The effect of this will be to expire the product template fragment for every product contained within the affected category.
141
-
142
- When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining (so the methods we define within the block are actually defined on that subscriber object). However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
159
+ When implementing the method definitions within the `subscribe_to` block, note that the block will be executed against an instance of a separate `Fragmentary::Subscriber` class that acts on behalf of the particular `Fragment` subclass we are defining. So the methods we define within the block are actually defined on that subscriber object. However, _all other_ methods called on the `Subscriber` object, including those called from within the methods we define in the block, are delegated by `method_missing` to the associated fragment subclass. So in the example, `touch_fragments_for_record` called from within `update_product_category_successful` represents `ProductTemplate.touch_fragments_for_record`. This method is defined by the `Fragment` class, so is available to all fragment subclasses.
143
160
 
144
161
  Note also that for fragment subclasses that declare `needs_record_id`, there is no need to define a `destroy_<model_name>_successful` method simply to remove a fragment whose `record_id` matches the `id` of a <model_name> application record that is destroyed, e.g. to destroy a `ProductTemplate` whose `record_id` matches the `id` of a destroyed `Product` object. Fragmentary handles this clean-up automatically. This is not to say, however, that 'destroy' handlers are never needed at all. The destruction of an application data record will often require other fragments to be touched.
145
162
 
@@ -158,7 +175,7 @@ A 'root' fragment is one that has no parent. In the template in which a root fra
158
175
 
159
176
  #### Nested Fragments
160
177
 
161
- The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` it takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
178
+ The variable `fragment` that is yielded to the block above is an object of class `Fragmentary::FragmentsHelper::CacheBuilder`, which contains both the actual ActiveRecord fragment record found or created by `cache_fragment` *and* the current template's execution context (i.e. the receiver of the `cache_fragment` method) as instance variables. The class has one public instance method defined, `cache_child`, which can be used to define a child fragment nested within the first. The method is used _within_ a block of content defined by `cache_fragment` and much like `cache_fragment` takes a hash of options that uniquely identify the child fragment. Also like `cache_fragment` it yields another `CacheBuilder` object and wraps a block containing the content of the child fragment to be cached.
162
179
  ```
163
180
  <% cache_fragment :type => 'ProductTemplate', :record_id => @product.id do |fragment| %>
164
181
  <% fragment.cache_child :type => 'StoresAvailable' do |child_fragment| %>
@@ -187,9 +204,11 @@ class StoresAvailable < Fragment
187
204
  acts_as_list_fragment :members => :product_stores, :list_record => :product
188
205
  end
189
206
  ```
207
+ The effect of this declaration is to ensure that whenever a new `ProductStore` is created, the `StoresAvailable` record for the corresponding product will be touched, so that the next time the list is rendered, the required new `AvailableStore` fragment will be created.
208
+
190
209
  `acts_as_list_fragment` takes two named arguments:
191
- * `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. The effect of declaring `acts_as_list_fragment` is to ensure that when that membership association is created, the list fragment it is being added to is touched, expiring the cache so that on the next request the list will be re-rendered, which has the effect of creating the required new `AvailableStore` fragment. Note that the value of the `members` argument should match the record type of the list item fragments. So in the example, the `AvailableStore` class should be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implied redundancy here that could be problematic; some adjustment may be made in the future).
192
- * `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the record_id (or the record itself; either will work) associated with the list fragment from a given membership association. If the value is a method name, the list record is found by calling that method on the membership association. In the example, the membership association is a `ProductStore` record, say `product_store`. The list is represented by a `StoresAvailable` fragment whose `record_id` points to a `Product` record. We can get that `Product` record simply by calling `product_store.product`, so the `list_record` parameter passed to `acts_as_list_fragment` is just the method `:product` (`:product_id` would also work). However sometimes a simple method like this is insufficient and a `Proc` may be used instead. In this case the newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
210
+ * `members` is the name of the list membership association in snake case, or tableized, form. The creation of an association of that type is what triggers the addition of a new item to the list. In the example, it is the creation of a new `product_store` that results in a new item being added to the list. Note that the value of the `members` argument should match the record type of the list item fragments (i.e. the type provided to `needs_record_id` in the definition of the list item fragment class). So in the example, the `AvailableStore` class would be defined with `needs_record_id, :type => ProductStore` (We recognize there's some implicit redundancy here that could be problematic; some adjustment may be made in the future).
211
+ * `list_record` is either the name of a method (represented by a symbol) or a `Proc` that defines how to obtain the `record_id` (or the record itself; either will work) associated with the list fragment from a given membership association. So, in the example, the membership association is a `ProductStore` record, say `product_store`, and `:product` is the method that would be called on `product_store` to produce the product whose `id` is the `record_id` of the `StoresAvailable` list fragment that needs to be touched when a new list item is created (`:product_id` would also work). Sometimes, however, a simple method like this is insufficient and a `Proc` may be used instead. The newly created membership association is passed as a parameter to the `Proc` and we can implement whatever functional relationship is necessary to obtain the list record. In this simple example, if we wanted (for no good reason) to use a `Proc`, it would look like `->(product_store){product_store.product}`.
193
212
 
194
213
  Note that in the example, the specified `list_record` method, `:product`, returns a single record for a given `product_store` membership association. However this isn't necessarily always the case. The method or `Proc` may also return an array of records or record_ids.
195
214
 
@@ -219,7 +238,7 @@ then creating an initial product_store association won't add the initial item si
219
238
  <% end %>
220
239
  ```
221
240
 
222
- It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example this would look like the following.
241
+ It is also worth noting that you are completely free to create lists without using `acts_as_list_fragment` at all. You just have to make sure that you explicitly provide a handler yourself in a `subscribe_to` block to touch any affected list fragments when a new membership association is created. In the example, this would look like the following.
223
242
  ```
224
243
  class StoresAvailable < Fragment
225
244
  subscribe_to 'ProductStore' do
@@ -230,13 +249,13 @@ class StoresAvailable < Fragment
230
249
  end
231
250
  ```
232
251
 
233
- In fact there can be cases where it is actually necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
252
+ In fact there can be cases where it is necessary to take an explicit approach like this. Using `acts_as_list_fragment` assumes that we can identify the list fragments to be touched by identifying their associated `record_id`s (this is the point of the method's `list_record` parameter). However, we have seen a situation where the set of list fragments that needed to be touched required a complex inner join between the `fragments` table and multiple application data tables, and this produced the list fragments to be touched directly rather than a set of associated record_ids.
234
253
 
235
254
  ### Accessing Fragments by Arbitrary Application Attributes
236
255
 
237
- The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the fragment type, the `parent_id` if the fragment is nested, and the `record_id`.
256
+ The examples above involved fragments associated with specific application data records via the `record_id` attribute. The fragments were uniquely identified by the `record_id`, the fragment type, and the `parent_id` if the fragment is nested.
238
257
 
239
- Of course it is not always necessary to have a `record_id`, for example in the case of an index page. On the other hand, there are also cases where something other than a `record_id` is needed to uniquely identify a fragment. Suppose, for example, you have a fragment that renders a set of books published in a certain year, a set of restaurants in a certain postcode, or a set of individuals (say sporting event competitors) in a particular age-group category. In cases like these we need to be able to uniquely identify the fragment again by its type and its `parent_id` (if it is nested), but also by some other parameter, which we refer to as a 'key' (the terminology may be a little unfortunate; this is not the same thing as a _cache key_).
258
+ Of course, it is not always necessary to have a `record_id`, for example in a fragment representing a list of items on an index page. On the other hand, there are also cases where something other than a `record_id` is needed in order to uniquely identify a fragment. Suppose, for example, you have a fragment that renders a set of books published in a certain year, a set of restaurants in a certain postcode, or a set of competitors in a sporting event within a particular age-group category. In cases like these we need to be able to uniquely identify the fragment again by its type and sometimes its `parent_id` (if it is nested), but also by some other parameter, which we refer to as a 'key' (the terminology may be a little unfortunate; this is not the same thing as a _cache key_).
240
259
 
241
260
  To facilitate this, the fragment model includes a `key` attribute that can be customized on a per-class basis using the `needs_key` method. For example,
242
261
  ```
@@ -253,7 +272,7 @@ With this definition we can define the cached content like this:
253
272
  <% end %>
254
273
  <% end %>
255
274
  ```
256
- Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could for example write `fragment.year_of_publication`.
275
+ Internally, the `key` attribute is a string, but the value of the custom option passed to either `cache_fragment` or `cache_child` (`year` in the example above) can be a string or anything that responds to `to_s`. Declaring `needs_key` also creates an instance method on your class with the same name as the key, so if you need to access the value of the key from the fragment, instead of writing `fragment.key` you could in this example write `fragment.year_of_publication`.
257
276
 
258
277
  ### User Specific Content
259
278
 
@@ -489,9 +508,9 @@ When application data that a cached fragment depends upon changes, i.e. as a res
489
508
 
490
509
  As part of this process, new fragment records may be created as *children* of an existing fragment if the existing fragment contains newly added list items. A new *root* fragment, on the other hand, will be created for any root fragment class defined with `needs_record_id` if an application data record of the associated `record_type` is created, as soon as a browser request is made for the new content (often a new page) containing that fragment.
491
510
 
492
- Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur.
511
+ Sometimes, however, it is desirable to avoid having to wait for an explicit request from a user in order to update the cache or to create new cached content. To deal with this, Fragmentary provides a mechanism to automatically create or refresh cached content preemptively, essentially as soon as a change in application data occurs. Rails' `ActionDispatch::Integration::Session` [class](https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb) provides an interface that allows requests to be sent directly to the application programmatically, without generating any external network traffic. Fragmentary uses this interface to automatically send requests needed to update the cache whenever changes in application data occur. These 'internal' requests are sent asynchronously so that they do not delay the response to the external POST, PATCH or DELETE request from the user's browser that brought about the change in application data.
493
512
 
494
- Creating these requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (in general, the request can be suppressed by passing `:no_request => true` if required). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
513
+ Creating internal requests is handled slightly differently depending on whether they are designed to update content associated with an existing root fragment or to create content for a new root fragment that does not yet exist. In the case of an existing root fragment, if the fragment's class has a `request_path` *instance* method defined, a request will be sent to the application at that path (represented in the form of a string) whenever `touch` is called on the fragment record (If required, the request can be suppressed by passing `:no_request => true` to `touch`). You simply need to define the `request_path` method in any individual `Fragment` subclass that you wish to generate requests for. For example:
495
514
  ```
496
515
  class ProductTemplate < Fragment
497
516
  needs_record_id :type => 'Product'
@@ -523,15 +542,15 @@ So in this example, any time a new `Product` record is created, Fragmentary will
523
542
 
524
543
  #### Request Queues
525
544
 
526
- A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, considering that different versions of some cached fragments exist for different user types, in order to ensure that all affected versions get refreshed we may need to send each internal request multiple times in the context of several different user sessions, each representing a different user type.
545
+ A single external HTTP POST, PATCH or DELETE request can cause changes to application data affecting multiple fragments on multiple pages. One external request can therefore lead to multiple internal application requests. In addition, given that different versions of some cached fragments may exist for different user types, we may need to send each internal request multiple times in the context of several different user sessions (representing different user types), to ensure that all affected versions get refreshed.
527
546
 
528
- To achieve this, during handling of the initial external request in which changes in application data propagate to affected fragment records via the `subscribe_to` declarations in your fragment class definitions, multiple instances of class `Fragmentary::RequestQueue`, each corresponding to a different user type, are used to store a collection of `Fragmentary::Request` objects representing the internal requests generated during that process.
547
+ To achieve this, during the handling of the initial external request that causes the application data to change, the internal requests created as a result are represented by instances of the class `Fragmentary::Request` and are initially stored in a number of 'queues' represented by instances of the class `Fragmentary::RequestQueue`. Separate queues exist for each of the different user types supported by the fragments to be rendered, and individual requests to a given endpoint are automatically placed in multiple queues as needed. However, responsibility for actually sending the requests stored in the queues is offloaded to an asynchronous process using Rails' `ActiveJob` interface (as detailed below under [Sending Queued Requests](https://github.com/MarkMT/fragmentary#sending-queued-requests)).
529
548
 
530
- The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user).
549
+ `Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports. So, for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`.
531
550
 
532
- Configuration of the `user_types` method is discussed in the next section. Fragmentary uses the types returned by this method to identify the request queues that each internal application request needs to be added to when instances of each particular `Fragment` subclass are updated. `Fragment` subclasses inherit both class and instance methods `request_queues` that return a hash of queues keyed by each of the specific user type strings that that specific subclass supports.
551
+ The set of user types that an individual `Fragment` subclass is expected to support is available via a configurable class method `user_types`, which returns an array of user type strings (Note this is different from the class method `user_type` discussed earlier that returns a single user type for a specific current user). Configuration of the `user_types` method is discussed in the next section.
533
552
 
534
- The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example, a `ProductTemplate` fragment may have two different request queues, `ProductTemplate.request_queues["admin"]` and `ProductTemplate.request_queues["signed_in"]`. If a `StoreTemplate` fragment has one request queue `StoreTemplate.request_queues["signed_in"]`, the `"signed_in"` queues for both classes represent the same object.
553
+ The request queue for any given user type is shared across all `Fragment` subclasses within the application. So for example,`ProductTemplate.request_queues["signed_in"]` and `StoreTemplate.request_queues["signed_in"]` both represent the same object.
535
554
 
536
555
  #### Configuring Internal Request Users
537
556
 
@@ -569,16 +588,34 @@ The value of the `:session_users` option (you can also use `:user_types` or just
569
588
 
570
589
  #### Sending Queued Requests
571
590
 
572
- A class method `Fragmentary::RequestQueue.all` returns an array of all request queues the application uses. The requests stored within a given `RequestQueue` can be sent to the application by calling the instance method `RequestQueue#send`. Calling `send` instantiates a `Fragmentary::UserSession` object representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` will sign in to the application using the credentials configured for the particular `user_type`.
591
+ Internal requests stored in the application's request queues are typically sent using an `after_action` filter in your `ApplicationController`, e.g. for create, update and destroy actions:
592
+ ```
593
+ class ApplicationController
594
+ after_action :send_queued_requests, :only => [:create, :update, :destroy]
595
+
596
+ def send_queued_requests
597
+ Fragmentary::RequestQueue.send_all(:between => 10.seconds)
598
+ end
599
+ end
600
+ ```
601
+ The optional named argument `between` represents the interval between individual requests in each queue being sent and allows you to throttle the request rate. If it is present (including `:between => 0.seconds`), the requests will be sent asynchronously using ActiveJob. If omitted, requests will be sent immediately (i.e before the response to the original external request is sent to the user's browser).
573
602
 
574
- To send all requests once processing of each external browser request has been completed, add a method such as the following to your `ApplicationController` class and call it using a controller `after_filter`, e.g. for create, update and destroy actions:
603
+ You can customize further by sending requests from queues individually. A class method `Fragmentary::RequestQueue.all` returns an array of all request queues that the application uses. The requests stored within an individual queue can be sent by calling the instance method `RequestQueue#send`.
575
604
  ```
576
605
  def send_queued_requests
577
606
  delay = 0.seconds
578
607
  Fragmentary::RequestQueue.all.each{|q| q.send(:delay => delay += 10.seconds)}
579
608
  end
580
609
  ```
581
- The `send` method takes two optional named arguments, `delay` and `between`. If neither are present, all requests held in the queue are sent immediately. If either are present, sending of requests is off-loaded to an asynchronous process using the [Delayed::Job gem](https://github.com/collectiveidea/delayed_job) (i.e. we are not currently using Active Job) and scheduled according to the parameters provided: `delay` represents the delay before the queue begins sending requests and `between` represents the interval between individual requests in the queue being sent. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. You may customize as appropriate.
610
+ `send` takes two optional named arguments, `delay` and `between`. `delay` represents the delay before the queue begins sending requests and `between` again represents the interval between individual requests. In the example above, we choose to delay the sending of requests from each queue by 10 seconds each. If either are present (including `:delay => 0`), the requests will be sent asynchronously using ActiveJob. If omitted, all requests held in the queue are sent immediately.
611
+
612
+ When requests are sent using either `send` or `send_all`, a `Fragmentary::UserSession` object is instantiated representing a browser session for the particular type of user the queue is handling. For sessions representing user types that need to be authenticated, instantiating the `UserSession` automatically signs in to the application using the credentials configured for the particular `user_type`.
613
+
614
+ ActiveJob supports a number of adapters for executing tasks asynchronously. You should configure according to your own needs. For example, to use Fragmentary with the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem, you would add the following to your `config/application.rb` file:
615
+ ```
616
+ config.active_job.queue_adapter = :delayed_job
617
+ ```
618
+ and add the 'delayed_job_active_record' gem to your Gemfile. Consult the [ActiveJob documentation](https://guides.rubyonrails.org/active_job_basics.html#backends) for more information as well as the documentation for your chosen adapter on how to start background task processes when you deploy your application.
582
619
 
583
620
  #### Queuing Requests Explicitly
584
621
 
@@ -628,31 +665,20 @@ class AvailableStore < Fragment
628
665
  end
629
666
  end
630
667
  ```
631
- While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process, which we again (currently) do using Delayed::Job.
668
+ While a `Handler` subclass definition defines a task to be run asynchronously, and calling the class method `create` within a `subscribe_to` block causes the task to be instantiated, we still have to explicitly dispatch the task to an asynchronous process. We again do this using ActiveJob in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
632
669
 
633
- To facilitate this we use the `Fragmentary::Dispatcher` class. A dispatcher object is instantiated with an array containing all existing handlers, retrieved using the class method `Fragmentary::Handler.all`.
634
670
  ```
635
- dispatcher = Fragmentary::Dispatcher.new(Fragmentary::Handler.all)
636
- ```
637
- The `Dispatcher` class defines an instance method `perform`, which invokes `call` on all the handlers provided when the dispatcher is instantiated. Delayed::Job uses the `perform` method to define the job to be run asynchronously, which is accomplished by passing the dispatcher object to `Delayed::Job.enqueue`:
638
- ```
639
- Delayed::Job.enqueue(dispatcher)
671
+ def handle_asynchronous_tasks
672
+ send_queued_requests # as defined earlier
673
+ Fragmentary::DispatchHandlersJob.perform_later(Fragmentary::Handler.all)
674
+ Fragmentary::Handler.clear
675
+ end
640
676
  ```
641
- As in the case of our queued background requests, we enqueue the dispatcher in an `ApplicationController` `after_filter`. In fact we can combine both the sending of background requests and the asynchronous fragment updating task in one method:
677
+ Here `Fragmentary::DispatchHandlersJob` is a subclass of `ActiveJob::Base`, which means if you wish you can specify additional options using the `set` method, e.g.
642
678
  ```
643
- class ApplicationController < ActionController::Base
644
-
645
- after_filter :handle_asynchronous_tasks
646
-
647
- def handle_asynchronous_tasks
648
- send_queued_requests # as defined earlier
649
- Delayed::Job.enqueue(Fragmentary::Dispatcher.new(Fragmentary::Handler.all))
650
- Fragmentary::Handler.clear
651
- end
652
-
653
- end
679
+ Fragmentary::DispatchHandlersJob.set(:wait => 1.minute, :queue => 'low_priority').perform_later(Fragmentary::Handler.all)
654
680
  ```
655
- Note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. The `Dispatcher` takes care of sending these additional requests.
681
+ Also note that updating fragments in an asynchronous process like this will itself generate internal application requests beyond those generated in the course of handling the user's initial request. `DispatchHandlersJob` also takes care of sending these additional requests.
656
682
 
657
683
  #### Updating Lists Asynchronously
658
684
 
@@ -790,7 +816,6 @@ Fragmentary.setup do |config|
790
816
  ...
791
817
  config.application_root_url_column = 'app_root'
792
818
  end
793
-
794
819
  ```
795
820
 
796
821
  Once this column has been added to the `fragments` table, any fragment records subsequently created by calls to `cache_fragment` or `cache_child` will have the column populated automatically based on the particular application instance in which the code is executed, and the content stored in the cache for each fragment record will be that generated by the corresponding instance. As long as any links rendered within the content are generated using Rails' path or url helpers, the cached content will be rendered correctly both when initially created and when retrieved subsequently.
@@ -803,11 +828,9 @@ The fact that two versions of content exist in the cache for each fragment means
803
828
 
804
829
  (Note: for better or worse, we've stuck with the term 'internal request' to mean any request delivered to the application programmatically in order to refresh cached content. This includes requests created by one application instance that are intended to be processed by another instance.)
805
830
 
806
- Our approach to sending requests between application instances relies on requests being processed asynchronously (i.e. by passing a `delay` value to RequestQueue#send in the controller method `send_queued_requests` discussed earlier). The reason is that asynchronous tasks used to process internal requests can be directed to specific task queues associated with the application instance they are intended to be processed by. Each application instance has an associated asynchronous task process. By configuring that process to run tasks from just the queue(s) designated for it, we ensure that any application instance can direct requests to any other.
831
+ Our approach to sending requests between application instances relies on processing requests asynchronously and directing the tasks used to do this to specific task queues associated with the application instance they are intended to be processed by. Each application instance has an associated asynchronous task process (in our case using Delayed::Job). By configuring that process to run tasks from just the queue(s) designated for it, we ensure that any application instance can direct requests to any other. As long as queued tasks are stored in a database table (as they are, for example, using Delayed::Job) and provided that each instance of the application has access to the same database, this approach achieves our objective. The way we accomplish this in practice is outlined below.
807
832
 
808
- As noted earlier, our current implementation relies on [Delayed::Job](https://github.com/collectiveidea/delayed_job) for creating and processing asynchronous tasks. Queued tasks are stored in a database table, so as long as each application and [Delayed::Job](https://github.com/collectiveidea/delayed_job) instance have access to the same database, this approach will be successful.
809
-
810
- To configure Fragmentary to automatically refresh cached content for multiple instances, first set `remote_urls` in `Fragmentary.config`. This is an array of root URLs for all _other_ instances of the application that requests should be sent to. For example, in order to allow requests to be sent from the production instance to the pre-release instance, in initializers/fragmentary.rb in the production code we would add the following configuration:
833
+ First, to configure Fragmentary to automatically refresh cached content for multiple instances, set `remote_urls` in `Fragmentary.config`. This is an array of root URLs for all _other_ instances of the application that requests should be sent to. For example, in order to allow requests to be sent from the production instance to the pre-release instance, in initializers/fragmentary.rb in the production code we would add the following configuration:
811
834
 
812
835
  ```
813
836
  Fragmentary.setup do |config|
@@ -818,38 +841,57 @@ end
818
841
 
819
842
  Our current approach to application deployment is to maintain different branches in our source repository for each application instance. This allows us to keep custom configurations like `config.remote_urls` above for each instance on their own branches. So in contrast to the case above, to allow requests to be sent from the pre-release instance to the production instance, in initializers/fragmentary.rb on the pre-release branch the `remote_urls` array would be set to `['http://myapp.com/']` .
820
843
 
821
- As an alternative, it may be possible to create a separate environment for the pre-release deployment in your config/environments directory and thus maintain the configuration for all application instances in a single repository branch. We have not investigated this approach.
822
-
823
- By specifying `remote_urls` as described above, for each internal application request Fragmentary will automatically create tasks to process the request not only on the application instance where it was created but also on all other instances corresponding to the elements in `remote_urls`. Fragmentary sends these tasks to [Delayed::Job](https://github.com/collectiveidea/delayed_job) queues that have names formed from the domain name and path of both the root URL of the current instance and the values in `remote_urls`. So in the example, a request to '/products/123', created say by editing a product name on the production website, would be sent to [Delayed::Job](https://github.com/collectiveidea/delayed_job) queues with names 'myapp.com' and 'myapp.com/prerelease/'.
824
-
825
- The final step needed to allow internal requests to be processed by the instance they are intended for is to configure the [Delayed::Job](https://github.com/collectiveidea/delayed_job) process associated with each instance to process tasks from the correct queue.
844
+ As an alternative, it would also be possible to create a separate environment for the pre-release deployment in your config/environments directory and thus maintain the configuration for all application instances in a single repository branch. We have not investigated this approach.
826
845
 
827
- In our case we use Capistrano 2 for deployment and use `delayed_job_args` in config/deploy.rb to configure queue names (see documentation [here](https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/recipes.rb)):
846
+ By specifying `remote_urls` as described above, for each internal application request Fragmentary will automatically create tasks to process the request not only on the application instance where it was created but also on all other instances corresponding to the elements in `remote_urls`. Fragmentary sends these tasks to named queues whose names are formed automatically from the domain name and path of both the root URL of the current instance and the values in `remote_urls`. So in the example, a request to '/products/123', created say by editing a product name on the production website, would be sent to queues with names 'myapp.com/' and 'myapp.com/prerelease/'.
828
847
 
848
+ The final step is to configure the asynchronous processes associated with each application instance to execute tasks from the correct queue. In our case, we use [Delayed::Job](https://github.com/collectiveidea/delayed_job) to handle asynchronous tasks and Capistrano 2 for deployment. We set `delayed_job_args` in `config/deploy.rb` to configure the queue names as follows (see documentation [here](https://github.com/collectiveidea/delayed_job/blob/master/lib/delayed/recipes.rb)):
829
849
  ```
830
850
  queue_prefix = "myapp.com/prerelease/"
831
- set :delayed_job_args, "--queue=#{queue_prefix},#{queue_prefix}_overnight"
851
+ set :delayed_job_args, "--queue=#{queue_prefix}"
832
852
  after "deploy:stop", "delayed_job:stop"
833
853
  after "deploy:start", "delayed_job:start"
834
854
  after "deploy:restart", "delayed_job:restart"
835
855
  ```
856
+ Again, this configuration will be different for each instance, and so in our case each one is stored in different source repository branches.
836
857
 
837
- Again, this configuration will be different for each instance and so in our case each one will be stored in different source repository branches.
838
-
839
- Note that if you configure [Delayed::Job](https://github.com/collectiveidea/delayed_job) to work only from specific queues, you'll need to make sure that _any_ asynchronous tasks created by your application are submitted to one of those queues.
858
+ Note that if you configure [Delayed::Job](https://github.com/collectiveidea/delayed_job) (or other backend) to work only from specific queues, you'll need to make sure that _any_ asynchronous tasks created by your application are submitted to one of those queues.
840
859
 
841
860
  ## Dependencies
842
861
 
843
- - The current implementation of Fragmentary has been tested using Rails 5. It does not work with earlier versions of Rails due to a change in the API for Rails `ActionDispatch::Integration::Session` class. We do have a 'Rails.4.2' branch that uses the older API in the github repository. However this branch is no longer maintained.
844
- - As noted, Fragmentary uses the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem to execute background tasks asynchronously.
862
+ - Fragmentary is currently being used in production with Rails 6.0. Versions >=0.4 do not work with earlier versions of Rails for a couple of reasons:
863
+ - The version of ActiveJob in Rails 5.x does not support the custom serialization needed to enqueue asynchronous tasks for sending internal requests. A repository branch 'delayed_job' is available that uses the [Delayed::Job](https://github.com/collectiveidea/delayed_job) gem directly (i.e. without ActiveJob) and this works well with Rails 5.x. However, this branch is no longer maintained.
864
+ - The API for the Rails `ActionDispatch::Integration::Session` class, which is used to handle internal application requests, was changed in Rails 5.0. Consequently the current version of Fragmentary is incompatible with Rails 4.x and earlier. We do have a 'Rails.4.2' branch in the repository that uses the older API. However, this branch is no longer maintained.
845
865
 
846
- ## Contributing
866
+ ## Timestamps
847
867
 
848
- Bug reports and usage questions are welcome at https://github.com/MarkMT/fragmentary.
868
+ Fragmentary allows HTML comments to be optionally inserted into cached fragments identifying the time at which the fragment was last cached. Simply add `config.insert_timestamps = true` to the configuration block in your `config/initializers/fragmentary.rb` file.
869
+
870
+ It is also possible to insert comments identifying a version name for the application code in use at the time the fragment was cached and the time at which that version was deployed. This is accomplished by dynamically adding values for the configuration settings `Fragmentary.config.release_name` and `Fragmentary.config.deployed_at` to your `config/initializers/fragmentary.rb` file at the time you deploy. ie. within your deployment script you need to arrange to append something like the following to that file.
871
+ ```
872
+ Fragmentary.config.release_name = release_name
873
+ Fragmentary.config.deployed_at = deployed_at
874
+ ```
875
+ It is up to you how you define/determine these values, but `deployed_at` would typically represent the value of `Time.now.utc` at the time the deployment script is executed. If these two configuration values are set, and `Fragmentary.config.insert_timestamps` is set to true, Fragmentary will automatically insert comments along the lines of the following:
876
+ ```
877
+ <!-- ProductDetails 2382 cached by Fragmentary version 0.4 at 2023-09-22 09:28:02 UTC -->
878
+ <!-- Cached using application release 20230919233149 deployed at 2023-09-19 23:33:16 UTC -->
879
+ ... Cached content appears here ...
880
+ <!-- ProductDetails 2382 ends -->
881
+ ```
882
+
883
+ ## Utility Methods
849
884
 
850
- ## Testing
885
+ The `Fragment` class and its subclasses provide a number of utility instance methods that are typically useful for interacting with fragments from the console.
886
+ - `touch_or_destroy` - recursively touches the fragment record and all decendant fragments for which cache entries exist. Destroys any fragment records in the tree for which no cache entry exists.
887
+ - `cache_exist?` - returns a boolean indicating whether fragment has content stored in the cache.
888
+ - `content` - retrieves the fragment's cached content
889
+ - `delete_cache` - removes the fragment's content from the cache.
890
+ - `delete_cache_tree` - recursively removes from the cache all content for the fragment and its descendants.
851
891
 
852
- You're welcome to write some tests!!!
892
+ ## Contributing
893
+
894
+ Bug reports and usage questions are welcome at https://github.com/MarkMT/fragmentary.
853
895
 
854
896
  ## License
855
897
 
data/fragmentary.gemspec CHANGED
@@ -22,8 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.bindir = "exe"
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_runtime_dependency "rails", "~> 5.0"
26
- spec.add_runtime_dependency "delayed_job_active_record", "~> 4.1"
25
+ spec.add_runtime_dependency "rails", "~> 6.0"
27
26
  spec.add_runtime_dependency "wisper-activerecord", "~> 1.0"
28
27
  spec.add_runtime_dependency "http", "~> 3.0.0"
29
28
  spec.add_runtime_dependency "nokogiri"
@@ -3,13 +3,17 @@ module Fragmentary
3
3
  class Config
4
4
  include Singleton
5
5
  attr_accessor :current_user_method, :get_sign_in_path, :post_sign_in_path, :sign_out_path,
6
- :users, :default_user_type_mapping, :session_users, :application_root_url_column, :remote_urls
6
+ :users, :default_user_type_mapping, :session_users, :application_root_url_column,
7
+ :remote_urls, :insert_timestamps, :deployed_at, :release_name
7
8
 
8
9
  def initialize
9
10
  # default
10
11
  @current_user_method = :current_user
11
12
  @application_root_url_column = :application_root_url
12
13
  @remote_urls = []
14
+ @insert_timestamps = false
15
+ @deployed_at = nil
16
+ @release_name = nil
13
17
  end
14
18
 
15
19
  def session_users=(session_users)
@@ -146,7 +146,7 @@ module Fragmentary
146
146
  def request_queues
147
147
  super # ensure that @@request_queues has been defined
148
148
  @request_queues ||= begin
149
- app_root_url = Rails.application.routes.url_helpers.root_url
149
+ app_root_url = Fragmentary.application_root_url
150
150
  remote_urls = Fragmentary.config.remote_urls
151
151
  user_types.each_with_object( Hash.new {|hsh0, url| hsh0[url] = {}} ) do |user_type, hsh|
152
152
  # Internal request queues
@@ -494,12 +494,15 @@ module Fragmentary
494
494
  end
495
495
 
496
496
  def touch(*args, no_request: false)
497
+ @no_request = no_request # stored for use in #touch_parent via the after_commit callback
497
498
  request_queues.each{|key, hsh| hsh.each{|key2, queue| queue << request}} if request && !no_request
498
499
  super(*args)
499
500
  end
500
501
 
502
+ # delete the associated cache content before destroying the fragment
501
503
  def destroy(options = {})
502
504
  options.delete(:delete_matches) ? delete_matched_cache : delete_cache
505
+ @no_request = options.delete(:no_request) # stored for use in #touch_parent via the after_commit callback
503
506
  super()
504
507
  end
505
508
 
@@ -508,9 +511,17 @@ module Fragmentary
508
511
  end
509
512
 
510
513
  def delete_cache
511
- cache_store.delete(ActiveSupport::Cache.expand_cache_key(self, 'views'))
514
+ cache_store.delete(fragment_key)
512
515
  end
513
516
 
517
+ # Recursively delete the cache entry for this fragment and all of its children
518
+ # Does NOT destroy the fragment or its children
519
+ def delete_cache_tree
520
+ children.each(&:delete_cache_tree)
521
+ delete_cache if cache_exist?
522
+ end
523
+
524
+ # Recursively touch the fragment and all of its children
514
525
  def touch_tree(no_request: false)
515
526
  children.each{|child| child.touch_tree(:no_request => no_request)}
516
527
  # If there are children, we'll have already touched this fragment in the process of touching them.
@@ -520,19 +531,34 @@ module Fragmentary
520
531
  # Touch this fragment and all descendants that have entries in the cache. Destroy any that
521
532
  # don't have cache entries.
522
533
  def touch_or_destroy
523
- puts " touch_or_destroy #{self.class.name} #{id}"
524
534
  if cache_exist?
525
535
  children.each(&:touch_or_destroy)
526
536
  # if there are children, this will be touched automatically once they are.
527
537
  touch(:no_request => true) unless children.any?
528
538
  else
529
- destroy # will also destroy all children because of :dependent => :destroy
539
+ destroy(:no_request => true) # will also destroy all children because of :dependent => :destroy
530
540
  end
531
541
  end
532
542
 
533
543
  def cache_exist?
534
544
  # expand_cache_key calls cache_key and prepends "views/"
535
- cache_store.exist?(ActiveSupport::Cache.expand_cache_key(self, 'views'))
545
+ cache_store.exist?(fragment_key)
546
+ end
547
+
548
+
549
+ # Typically used along with #cache_exist? when testing from the console.
550
+ # Note that both methods will only return correct results for fragments associated with the application_root_url
551
+ # (either root or children) corresponding to the particular console session in use. i.e. you can't see into the
552
+ # production cache from a prerelease console session and vice versa.
553
+ def content
554
+ cache_store.read(fragment_key)
555
+ end
556
+
557
+ # This emulates the result of passing the fragment object to AbstractController::Caching::Fragments#combined_fragment_cache_key
558
+ # when the cache helper method invokes controller.read_fragment from the view. The result can be passed to ActiveSupport::Cache methods
559
+ # #read, #write, #fetch, #delete, and #exist?
560
+ def fragment_key
561
+ ['views', self]
536
562
  end
537
563
 
538
564
  # Request-related methods...
@@ -570,7 +596,8 @@ module Fragmentary
570
596
 
571
597
  private
572
598
  def touch_parent
573
- parent.try :touch unless previous_changes["memo"]
599
+ parent.try(:touch, {:no_request => @no_request}) unless previous_changes["memo"]
600
+ @no_request = false
574
601
  end
575
602
 
576
603
  module NeedsRecordId
@@ -3,7 +3,7 @@ module Fragmentary
3
3
  module FragmentsHelper
4
4
 
5
5
  def cache_fragment(options, &block)
6
- options.reverse_merge!(Fragmentary.config.application_root_url_column => self.root_url.gsub(%r{https?://}, ''))
6
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
7
7
  CacheBuilder.new(self).cache_fragment(options, &block)
8
8
  end
9
9
 
@@ -11,7 +11,7 @@ module Fragmentary
11
11
  # the template option is deprecated but avoids breaking prior usage
12
12
  template = options.delete(:template) || self
13
13
  options.reverse_merge!(:user => Template.new(template).current_user)
14
- options.reverse_merge!(Fragmentary.config.application_root_url_column => self.root_url.gsub(%r{https?://}, ''))
14
+ options.reverse_merge!(Fragmentary.config.application_root_url_column => Fragmentary.application_root_url.gsub(%r{https?://}, ''))
15
15
  CacheBuilder.new(template, Fragmentary::Fragment.base_class.existing(options))
16
16
  end
17
17
 
@@ -36,7 +36,16 @@ module Fragmentary
36
36
  builder = CacheBuilder.new(@template, next_fragment)
37
37
  unless no_cache
38
38
  @template.cache next_fragment, :skip_digest => true do
39
- yield(builder)
39
+ if Fragmentary.config.insert_timestamps
40
+ @template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} cached by Fragmentary version #{VERSION} at #{Time.now.utc} -->")
41
+ if deployed_at && release_name
42
+ @template.safe_concat("<!-- Cached using application release #{release_name} deployed at #{deployed_at} -->")
43
+ end
44
+ yield(builder)
45
+ @template.safe_concat("<!-- #{next_fragment.type} #{next_fragment.id} ends -->")
46
+ else
47
+ yield(builder)
48
+ end
40
49
  end
41
50
  else
42
51
  yield(builder)
@@ -52,6 +61,13 @@ module Fragmentary
52
61
  @fragment.send(method, *args)
53
62
  end
54
63
 
64
+ def deployed_at
65
+ @deployed_at ||= Fragmentary.config.deployed_at
66
+ end
67
+
68
+ def release_name
69
+ @release_name ||= Fragmentary.config.release_name
70
+ end
55
71
  end
56
72
 
57
73
  end
@@ -1,4 +1,26 @@
1
1
  module Fragmentary
2
+
3
+ class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::Handler
7
+ end
8
+
9
+ def serialize(handler)
10
+ super(
11
+ {
12
+ :class_name => handler.class.name,
13
+ :args => handler.args
14
+ }
15
+ )
16
+ end
17
+
18
+ def deserialize(hsh)
19
+ hsh[:class_name].constantize.new(hsh[:args])
20
+ end
21
+
22
+ end
23
+
2
24
  class Handler
3
25
  def self.all
4
26
  @@all
@@ -14,6 +36,8 @@ module Fragmentary
14
36
  handler
15
37
  end
16
38
 
39
+ attr_reader :args
40
+
17
41
  def initialize(**args)
18
42
  @args = args
19
43
  end
@@ -1,12 +1,11 @@
1
+ require 'fragmentary/serializers/handler_serializer'
2
+
1
3
  module Fragmentary
2
4
 
3
- class Dispatcher
4
- def initialize(tasks)
5
- @tasks = tasks
6
- end
5
+ class DispatchHandlersJob < ActiveJob::Base
7
6
 
8
- def perform
9
- @tasks.each do |task|
7
+ def perform(tasks)
8
+ tasks.each do |task|
10
9
  Rails.logger.info "***** Dispatching task for handler class #{task.class.name}"
11
10
  task.call
12
11
  end
@@ -14,6 +13,7 @@ module Fragmentary
14
13
  queue.start
15
14
  end
16
15
  end
16
+
17
17
  end
18
18
 
19
19
  end
@@ -0,0 +1,26 @@
1
+ require 'fragmentary/serializers/request_queue_serializer'
2
+
3
+ module Fragmentary
4
+
5
+ class SendRequestsJob < ActiveJob::Base
6
+
7
+ after_perform :schedule_next
8
+
9
+ def perform(queue, delay: nil, between: nil, queue_suffix: '', priority: 0)
10
+ @queue = queue
11
+ @delay = delay
12
+ @between = between
13
+ @queue_suffix = queue_suffix
14
+ @priority = priority
15
+ @between ? @queue.send_next_request : @queue.send_all_requests
16
+ end
17
+
18
+ def schedule_next
19
+ if @queue.size > 0
20
+ self.enqueue(:wait => @between, :queue => @queue.target.queue_name + @queue_suffix, :priority => @priority)
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
@@ -6,6 +6,18 @@ module Fragmentary
6
6
  @@all ||= []
7
7
  end
8
8
 
9
+ def self.send_all(between: nil)
10
+ unless between
11
+ all.each{|q| q.start}
12
+ else
13
+ unless between.is_a? ActiveSupport::Duration
14
+ raise TypeError, "Fragmentary::RequestQueue.send_all requires the keyword argument :between to be of class ActiveSupport::Duration. The value provided is of class #{between.class.name}."
15
+ end
16
+ delay = 0.seconds
17
+ all.each{|q| q.start(:delay => delay += between)}
18
+ end
19
+ end
20
+
9
21
  attr_reader :requests, :user_type, :host_root_url
10
22
 
11
23
  def initialize(user_type, host_root_url)
@@ -72,9 +84,10 @@ module Fragmentary
72
84
  def queue_name
73
85
  @url.gsub(%r{https?://}, '')
74
86
  end
87
+
75
88
  end
76
89
 
77
- attr_reader :queue
90
+ attr_reader :queue, :target, :delay, :between, :queue_suffix, :priority
78
91
 
79
92
  def initialize(queue)
80
93
  @queue = queue
@@ -90,9 +103,9 @@ module Fragmentary
90
103
  end
91
104
 
92
105
  # Send all requests, either directly or by schedule
93
- def start(delay: nil, between: nil)
106
+ def start(delay: nil, between: nil, queue_suffix: '', priority: 0)
94
107
  Rails.logger.info "\n***** Processing request queue for user_type '#{queue.user_type}'\n"
95
- @delay = delay; @between = between
108
+ @delay = delay; @between = between; @queue_suffix = queue_suffix; @priority = priority
96
109
  if @delay or @between
97
110
  schedule_requests(@delay)
98
111
  # sending requests by schedule makes a copy of the sender and queue objects for
@@ -115,10 +128,6 @@ module Fragmentary
115
128
  end
116
129
  end
117
130
 
118
- def success
119
- schedule_requests(@between) if queue.size > 0
120
- end
121
-
122
131
  private
123
132
 
124
133
  def send_all_requests
@@ -130,10 +139,8 @@ module Fragmentary
130
139
  def schedule_requests(delay=0.seconds)
131
140
  if queue.size > 0
132
141
  clear_session
133
- Delayed::Job.transaction do
134
- self.class.jobs.destroy_all
135
- Delayed::Job.enqueue self, :run_at => delay.from_now, :queue => @target.queue_name
136
- end
142
+ job = SendRequestsJob.new(queue, delay: delay, between: between, queue_suffix: queue_suffix, priority: priority)
143
+ job.enqueue(:wait => delay, :queue => target.queue_name + queue_suffix, :priority => priority)
137
144
  end
138
145
  end
139
146
 
@@ -0,0 +1,24 @@
1
+ module Fragmentary
2
+
3
+ class HandlerSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::Handler
7
+ end
8
+
9
+ def serialize(handler)
10
+ super(
11
+ {
12
+ :class_name => handler.class.name,
13
+ :args => handler.args
14
+ }
15
+ )
16
+ end
17
+
18
+ def deserialize(hsh)
19
+ hsh[:class_name].constantize.new(hsh[:args])
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,36 @@
1
+ module Fragmentary
2
+
3
+ class RequestQueueSerializer < ActiveJob::Serializers::ObjectSerializer
4
+
5
+ def serialize?(arg)
6
+ arg.is_a? Fragmentary::RequestQueue
7
+ end
8
+
9
+ def serialize(queue)
10
+ super(
11
+ {
12
+ :user_type => queue.user_type,
13
+ :host_root_url => queue.host_root_url,
14
+ :requests => queue.requests.map do |r|
15
+ {
16
+ :method => r.method,
17
+ :path => r.path,
18
+ :parameters => r.parameters,
19
+ :opinions => r.options
20
+ }
21
+ end
22
+ }
23
+ )
24
+ end
25
+
26
+ def deserialize(hsh)
27
+ queue = RequestQueue.new(hsh[:user_type], hsh[:host_root_url])
28
+ hsh[:requests].each do |r|
29
+ queue << Request.new(r[:method], r[:path], r[:parameters], r[:options] || {})
30
+ end
31
+ queue
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -22,7 +22,7 @@ module Fragmentary
22
22
  end
23
23
 
24
24
  def session_host
25
- @session_host ||= @target.host + (port=@target.port ? ":#{port}" : "")
25
+ @session_host ||= @target.host + ((port=@target.port) ? ":#{port}" : "")
26
26
  end
27
27
 
28
28
  def session_sign_in_path
@@ -83,8 +83,10 @@ module Fragmentary
83
83
  options.merge!({:params => parameters})
84
84
  options.merge!(session_options)
85
85
  if options.try(:[], :xhr)
86
+ puts " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
86
87
  Rails.logger.info " * Sending xhr request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
87
88
  else
89
+ puts " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
88
90
  Rails.logger.info " * Sending request '#{method.to_s} #{path}'" + (!parameters.nil? ? " with #{parameters.inspect}" : "")
89
91
  end
90
92
  @session.send(method, path, options)
@@ -1,3 +1,3 @@
1
1
  module Fragmentary
2
- VERSION = "0.3"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/fragmentary.rb CHANGED
@@ -11,6 +11,8 @@ require 'fragmentary/session_user'
11
11
  require 'fragmentary/widget_parser'
12
12
  require 'fragmentary/widget'
13
13
  require 'fragmentary/publisher'
14
+ require 'fragmentary/jobs/send_requests_job'
15
+ require 'fragmentary/jobs/dispatch_handlers_job'
14
16
 
15
17
  module Fragmentary
16
18
  def self.config
@@ -19,4 +21,12 @@ module Fragmentary
19
21
  @config
20
22
  end
21
23
  class << self; alias setup config; end
24
+
25
+ def self.application
26
+ Rails.application
27
+ end
28
+
29
+ def self.application_root_url
30
+ application.routes.url_helpers.root_url
31
+ end
22
32
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fragmentary
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.3'
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mark Thomson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-13 00:00:00.000000000 Z
11
+ date: 2023-11-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,28 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '5.0'
19
+ version: '6.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '5.0'
27
- - !ruby/object:Gem::Dependency
28
- name: delayed_job_active_record
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - "~>"
32
- - !ruby/object:Gem::Version
33
- version: '4.1'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - "~>"
39
- - !ruby/object:Gem::Version
40
- version: '4.1'
26
+ version: '6.0'
41
27
  - !ruby/object:Gem::Dependency
42
28
  name: wisper-activerecord
43
29
  requirement: !ruby/object:Gem::Requirement
@@ -142,13 +128,16 @@ files:
142
128
  - fragmentary.gemspec
143
129
  - lib/fragmentary.rb
144
130
  - lib/fragmentary/config.rb
145
- - lib/fragmentary/dispatcher.rb
146
131
  - lib/fragmentary/fragment.rb
147
132
  - lib/fragmentary/fragments_helper.rb
148
133
  - lib/fragmentary/handler.rb
134
+ - lib/fragmentary/jobs/dispatch_handlers_job.rb
135
+ - lib/fragmentary/jobs/send_requests_job.rb
149
136
  - lib/fragmentary/publisher.rb
150
137
  - lib/fragmentary/request.rb
151
138
  - lib/fragmentary/request_queue.rb
139
+ - lib/fragmentary/serializers/handler_serializer.rb
140
+ - lib/fragmentary/serializers/request_queue_serializer.rb
152
141
  - lib/fragmentary/session_user.rb
153
142
  - lib/fragmentary/subscriber.rb
154
143
  - lib/fragmentary/subscription.rb
@@ -175,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
175
164
  - !ruby/object:Gem::Version
176
165
  version: '0'
177
166
  requirements: []
178
- rubygems_version: 3.0.8
167
+ rubygems_version: 3.0.9
179
168
  signing_key:
180
169
  specification_version: 4
181
170
  summary: Fragment modeling and caching for Rails