inertia_rails_ssr_cache 0.1.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 +7 -0
- data/LICENSE.md +319 -0
- data/README.md +196 -0
- data/lib/inertia_rails_ssr_cache/configuration.rb +111 -0
- data/lib/inertia_rails_ssr_cache/probe.rb +121 -0
- data/lib/inertia_rails_ssr_cache/railtie.rb +21 -0
- data/lib/inertia_rails_ssr_cache/result.rb +92 -0
- data/lib/inertia_rails_ssr_cache/version.rb +12 -0
- data/lib/inertia_rails_ssr_cache/warm_job.rb +52 -0
- data/lib/inertia_rails_ssr_cache.rb +50 -0
- data/sig/framework_boundary.rbs +31 -0
- data/sig/inertia_rails_ssr_cache.rbs +70 -0
- metadata +174 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 71e26cafbbad0fef59528cf35fa5094b095a40b1488abfc0a229415e3cc697b0
|
|
4
|
+
data.tar.gz: a75e0b616adfad90ac03b1161c3a3a6173b8fbbaadba928a05a6439d017bcd18
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b998c678608716c0dad23b2c52707569523b4b7c87f650ba5972c00add0d1337570c7f81a9587fda8191117d09056365bc82234d13f3013063f3fcfbef34f0a1
|
|
7
|
+
data.tar.gz: 72bfd354ae73778c477a42d87dae24df7c93a3b20738e383a32bef2a164be1819f6762ee24c3bfae65caa3351bf17f84d4508ef2c8de774dbbd43ebf7b31aad7
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# The Inertia Rails SSR Cache License
|
|
2
|
+
|
|
3
|
+
The Inertia Rails SSR Cache License, version 1.0, an application of ShakaCode
|
|
4
|
+
Trust-Based Commercial Licensing.
|
|
5
|
+
|
|
6
|
+
Copyright (c) 2026 ShakaCode LLC, a Hawaii limited liability company
|
|
7
|
+
("ShakaCode").
|
|
8
|
+
|
|
9
|
+
SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
10
|
+
|
|
11
|
+
## Plain-language summary (informative, not part of the terms)
|
|
12
|
+
|
|
13
|
+
Inertia Rails SSR Cache is trust-based: there is no license key, nothing
|
|
14
|
+
phones home, and nothing in the software blocks, slows, or changes how
|
|
15
|
+
your app runs based on license status. Reading the source, development,
|
|
16
|
+
test, CI, staging, review apps, education, personal projects, and
|
|
17
|
+
open-source work are free for everyone, at any organization size.
|
|
18
|
+
Production use is also free if you can answer yes to all three
|
|
19
|
+
questions: *Do fewer than 10 people work for your organization (counting
|
|
20
|
+
all affiliates)? Was its revenue in the last twelve months under US $1M?
|
|
21
|
+
Has it raised under US $1M in outside capital, ever?* Charities,
|
|
22
|
+
educational institutions, and hospitals are free at any size. If any
|
|
23
|
+
answer is no and you are none of those, production use requires a
|
|
24
|
+
qualifying subscription: an active ShakaStack Pro subscription, which
|
|
25
|
+
includes React on Rails Pro, covers organizations under 250 people and
|
|
26
|
+
under US $10M revenue, and covers the whole organization: every
|
|
27
|
+
application, environment, and developer. Larger organizations need a
|
|
28
|
+
separate agreement. See https://pro.reactonrails.com/. Once a version
|
|
29
|
+
ships under these terms, those terms govern that version permanently.
|
|
30
|
+
|
|
31
|
+
## 1. Acceptance
|
|
32
|
+
|
|
33
|
+
By using the software, you agree to these terms. If you use the software
|
|
34
|
+
on behalf of an organization, you represent that you have authority to
|
|
35
|
+
bind that organization, and "you" includes it.
|
|
36
|
+
|
|
37
|
+
## 2. Definitions
|
|
38
|
+
|
|
39
|
+
**"Software"** means Inertia Rails SSR Cache: the
|
|
40
|
+
`inertia_rails_ssr_cache` Ruby gem, the `inertia-rails-ssr-cache` npm
|
|
41
|
+
package, and any other component ShakaCode distributes under these terms.
|
|
42
|
+
Inertia Rails, the Inertia.js adapters, and other dependencies that the
|
|
43
|
+
software works with are licensed separately by their own authors.
|
|
44
|
+
|
|
45
|
+
**"Control"** means ownership of more than 50% of the voting interests of
|
|
46
|
+
an entity, or the power to direct its management or policies, by contract
|
|
47
|
+
or otherwise.
|
|
48
|
+
|
|
49
|
+
**"Your organization"** means you together with every entity that Controls
|
|
50
|
+
you, that you Control, or that is under common Control with you. Teams,
|
|
51
|
+
departments, and other sub-units are not separate organizations.
|
|
52
|
+
|
|
53
|
+
**"Small Organization"** means an organization for which all of the
|
|
54
|
+
following are true, counted across the entire organization as defined
|
|
55
|
+
above:
|
|
56
|
+
|
|
57
|
+
a. fewer than 10 individuals perform paid work for it or on its products
|
|
58
|
+
and services, whether directly or through staffing, outsourcing, or
|
|
59
|
+
services arrangements, counted as full-time equivalents;
|
|
60
|
+
b. its total gross revenue, measured over the twelve months ending with
|
|
61
|
+
the most recently completed calendar quarter, was less than
|
|
62
|
+
US $1,000,000; and
|
|
63
|
+
c. it has received less than US $1,000,000 in aggregate outside capital
|
|
64
|
+
over its lifetime (equity, tokens, convertible instruments, venture or
|
|
65
|
+
investor debt, accelerator funding, and grants or donations, other than
|
|
66
|
+
academic research or charitable grants to accredited academic or
|
|
67
|
+
nonprofit institutions) from sources other than its own operating
|
|
68
|
+
revenue and its founders' personal funds. Ordinary-course bank and
|
|
69
|
+
trade credit do not count.
|
|
70
|
+
|
|
71
|
+
Government entities, other than public educational institutions, are not
|
|
72
|
+
Small Organizations.
|
|
73
|
+
|
|
74
|
+
**"Charity"** means an organization recognized as charitable or
|
|
75
|
+
public-benefit under the law of the jurisdiction where it is organized (for
|
|
76
|
+
example, a US 501(c)(3) organization or its equivalent elsewhere), no part
|
|
77
|
+
of whose net earnings benefits any private person.
|
|
78
|
+
|
|
79
|
+
**"Educational Institution"** means a school, college, university, or
|
|
80
|
+
other institution whose principal purpose is teaching or academic
|
|
81
|
+
research, whether public or private, including its libraries, research
|
|
82
|
+
institutes, and teaching hospitals.
|
|
83
|
+
|
|
84
|
+
**"Hospital"** means an institution licensed to provide inpatient
|
|
85
|
+
medical care to the public, whether public, non-profit, or private,
|
|
86
|
+
including its clinics and medical research units.
|
|
87
|
+
|
|
88
|
+
Charity, Educational Institution, and Hospital status belongs to the
|
|
89
|
+
qualifying entity and to affiliates that qualify on their own; it does
|
|
90
|
+
not extend to for-profit affiliates, spin-offs, or commercial ventures
|
|
91
|
+
that are not themselves one of these.
|
|
92
|
+
|
|
93
|
+
**"Production use"** means running the software to serve an application,
|
|
94
|
+
website, or service to its end users, whether those users are the public,
|
|
95
|
+
your customers, or your own staff. Development, test, continuous
|
|
96
|
+
integration, staging, preview, review-app, and other non-production
|
|
97
|
+
environments are not production use.
|
|
98
|
+
|
|
99
|
+
**"Subscription"** means an active paid subscription that ShakaCode
|
|
100
|
+
designates as including the software: a ShakaStack Pro subscription
|
|
101
|
+
(which includes React on Rails Pro) held by an organization that has
|
|
102
|
+
fewer than 250 individuals counted as in clause (a) of the Small
|
|
103
|
+
Organization definition and less than US $10,000,000 in total gross
|
|
104
|
+
revenue measured as in clause (b), purchased at
|
|
105
|
+
https://pro.reactonrails.com/ or through another order channel ShakaCode
|
|
106
|
+
designates; or a separate written agreement with ShakaCode that covers
|
|
107
|
+
the software. Any other organization needs such a separate written
|
|
108
|
+
agreement. Sponsorships and donations that ShakaCode has not designated
|
|
109
|
+
as subscriptions are appreciated but are not Subscriptions and grant no
|
|
110
|
+
production-use rights.
|
|
111
|
+
|
|
112
|
+
## 3. Copyright grant
|
|
113
|
+
|
|
114
|
+
ShakaCode grants you a non-exclusive, worldwide, royalty-free license to
|
|
115
|
+
use, copy, modify, and create derivative works of the software, and to
|
|
116
|
+
include it in the builds and deployments of your organization's own
|
|
117
|
+
applications, in each case only as allowed by these terms. You may share
|
|
118
|
+
copies with contractors and service providers working on those
|
|
119
|
+
applications, who must observe these terms. You may not distribute the
|
|
120
|
+
software or your modifications as a standalone product or library, or as
|
|
121
|
+
part of a product whose purpose is to provide the software's
|
|
122
|
+
functionality to others (see Section 7).
|
|
123
|
+
|
|
124
|
+
## 4. Free uses
|
|
125
|
+
|
|
126
|
+
The following uses are free of charge for anyone, at any organization
|
|
127
|
+
size:
|
|
128
|
+
|
|
129
|
+
a. reading and studying the source code;
|
|
130
|
+
b. development, test, continuous integration, staging, preview,
|
|
131
|
+
review-app, and every other non-production environment, without time
|
|
132
|
+
limit;
|
|
133
|
+
c. evaluating the software in production for up to 45 days per
|
|
134
|
+
organization (not per version, person, or application), solely to
|
|
135
|
+
decide whether to adopt it;
|
|
136
|
+
d. education and training of people, tutorials, demonstrations, and
|
|
137
|
+
academic research, in each case not directed at developing or
|
|
138
|
+
operating your organization's own products or services;
|
|
139
|
+
e. personal and hobby projects;
|
|
140
|
+
f. developing and operating a public open-source project distributed
|
|
141
|
+
under a license approved by the Open Source Initiative, where the
|
|
142
|
+
software runs as part of that project's own publicly available code
|
|
143
|
+
and primarily for that project's benefit, and not for the development
|
|
144
|
+
or operation of a proprietary product or service.
|
|
145
|
+
|
|
146
|
+
In addition, **all production use is free of charge for a Small
|
|
147
|
+
Organization, a Charity, an Educational Institution, or a Hospital, at
|
|
148
|
+
any size for the last three.** If your organization is one of these,
|
|
149
|
+
nothing in Section 5 applies to you.
|
|
150
|
+
|
|
151
|
+
## 5. Paid uses
|
|
152
|
+
|
|
153
|
+
Production use by an organization that is not a Small Organization, a
|
|
154
|
+
Charity, an Educational Institution, or a Hospital requires a
|
|
155
|
+
Subscription. This includes, without limitation, production use beyond
|
|
156
|
+
the Section 4(c) evaluation window, internal applications used only by
|
|
157
|
+
your staff, and applications you operate for your customers.
|
|
158
|
+
|
|
159
|
+
In any conflict between Section 4 and this Section 5, use described in
|
|
160
|
+
this Section 5 is production use and is not a free use under Section
|
|
161
|
+
4(b), (c), or (d).
|
|
162
|
+
|
|
163
|
+
**Client work.** If you use the software to build or run an application
|
|
164
|
+
for a client, the use is attributed to the organization whose business
|
|
165
|
+
the application ultimately serves, traced through any intermediaries.
|
|
166
|
+
That organization's status determines whether the use is free or paid,
|
|
167
|
+
and any required Subscription must be held by, or purchased for the
|
|
168
|
+
benefit of and identifying, that organization. You may rely on a client's
|
|
169
|
+
written self-certification of its status. A consulting firm's or agency's
|
|
170
|
+
own applications are attributed to the firm or agency.
|
|
171
|
+
|
|
172
|
+
## 6. Subscriptions
|
|
173
|
+
|
|
174
|
+
One Subscription covers your whole organization: every application,
|
|
175
|
+
environment, server, and developer of your organization, with no per-seat
|
|
176
|
+
or per-application charge. A Subscription is governed by the subscription
|
|
177
|
+
terms, order form, or other agreement under which it is purchased; if
|
|
178
|
+
that agreement conflicts with these terms, that agreement controls. While
|
|
179
|
+
a Subscription is active, ShakaCode provides software updates and
|
|
180
|
+
reasonable support through its published channels; no response or
|
|
181
|
+
resolution times are guaranteed unless an order or separate agreement
|
|
182
|
+
says so. If a required Subscription expires or is terminated, your
|
|
183
|
+
production use remains licensed for 30 days while you renew or wind down,
|
|
184
|
+
and the software keeps running either way. ShakaCode and you may enter a
|
|
185
|
+
separate written agreement that supplements or supersedes these terms.
|
|
186
|
+
|
|
187
|
+
## 7. No hosted or embedded offering
|
|
188
|
+
|
|
189
|
+
You may not provide the software or its cache-warming functionality to third
|
|
190
|
+
parties as a hosted, managed, embedded, or bundled offering, for example
|
|
191
|
+
a platform that builds or runs other organizations' Inertia Rails
|
|
192
|
+
applications, without a separate written agreement with ShakaCode. Each
|
|
193
|
+
organization whose applications use the software must independently
|
|
194
|
+
qualify for free use or hold its own Subscription. Running your own
|
|
195
|
+
applications for your own users, customers, or staff is not an offering
|
|
196
|
+
to third parties.
|
|
197
|
+
|
|
198
|
+
## 8. Trust, self-certification, and true-up
|
|
199
|
+
|
|
200
|
+
These terms are enforced by trust, not technology: the software contains
|
|
201
|
+
no license key, no activation, no usage telemetry, and no runtime
|
|
202
|
+
lockout. You are responsible for determining in good faith whether your
|
|
203
|
+
use is free or paid.
|
|
204
|
+
|
|
205
|
+
If your organization stops qualifying as a Small Organization, your
|
|
206
|
+
production use remains licensed for 60 days after the change; after that,
|
|
207
|
+
continued production use requires a Subscription. No payment is owed for
|
|
208
|
+
use during that window, and the obligation is not avoided by
|
|
209
|
+
re-qualification within 12 months of the change.
|
|
210
|
+
|
|
211
|
+
If you discover in good faith that past use required a Subscription,
|
|
212
|
+
purchasing the applicable Subscription within 60 days of discovery is
|
|
213
|
+
full compliance, and no back payment is owed for the good-faith period.
|
|
214
|
+
If your organization made production use requiring a Subscription
|
|
215
|
+
without a good-faith basis for treating it as free, ShakaCode may require
|
|
216
|
+
payment of the applicable subscription fees for the period of that use,
|
|
217
|
+
up to 24 months.
|
|
218
|
+
|
|
219
|
+
You must not misrepresent your organization, its size or status, or your
|
|
220
|
+
license status.
|
|
221
|
+
|
|
222
|
+
## 9. Redistribution and notices
|
|
223
|
+
|
|
224
|
+
Copies of the software and derivative works that these terms allow you to
|
|
225
|
+
make must remain under these same terms and must retain these terms, all
|
|
226
|
+
copyright and attribution notices, and the SPDX identifier.
|
|
227
|
+
|
|
228
|
+
## 10. Patents
|
|
229
|
+
|
|
230
|
+
ShakaCode grants you a license under any patent claims it can license that
|
|
231
|
+
are necessarily infringed by the software, only for uses allowed by these
|
|
232
|
+
terms. This patent license ends immediately if you or your organization
|
|
233
|
+
initiate a patent claim (excluding counterclaims and cross-claims filed in
|
|
234
|
+
response to a patent claim first brought against you) alleging that the
|
|
235
|
+
software infringes a patent.
|
|
236
|
+
|
|
237
|
+
## 11. Trademarks
|
|
238
|
+
|
|
239
|
+
These terms grant no rights in the names, logos, or trademarks of
|
|
240
|
+
ShakaCode or Inertia Rails SSR Cache, except for truthful factual
|
|
241
|
+
references.
|
|
242
|
+
|
|
243
|
+
## 12. Per-version terms, breach, and cure
|
|
244
|
+
|
|
245
|
+
These terms apply per version: the terms delivered with a version of the
|
|
246
|
+
software govern that version permanently, and patch and security releases
|
|
247
|
+
of a version line remain under that line's terms. Later versions may be
|
|
248
|
+
offered under different terms.
|
|
249
|
+
|
|
250
|
+
If you breach these terms, your license ends automatically 30 days after
|
|
251
|
+
ShakaCode provides written notice of the breach, unless you cure it within
|
|
252
|
+
that period; upon cure, your license is reinstated retroactively. Curing
|
|
253
|
+
continued production use during a lapsed Subscription includes payment of
|
|
254
|
+
the applicable fees for the lapsed period.
|
|
255
|
+
|
|
256
|
+
## 13. Feedback
|
|
257
|
+
|
|
258
|
+
If you submit feedback, suggestions, or improvements to ShakaCode, you
|
|
259
|
+
grant ShakaCode a worldwide, royalty-free, irrevocable license to use and
|
|
260
|
+
incorporate them.
|
|
261
|
+
|
|
262
|
+
## 14. Disclaimer and limitation of liability
|
|
263
|
+
|
|
264
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
265
|
+
IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
266
|
+
PURPOSE, TITLE, AND NON-INFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY
|
|
267
|
+
LAW, SHAKACODE WILL NOT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL,
|
|
268
|
+
OR CONSEQUENTIAL DAMAGES ARISING FROM OR RELATED TO THE SOFTWARE OR THESE
|
|
269
|
+
TERMS, AND SHAKACODE'S TOTAL LIABILITY WILL NOT EXCEED THE AMOUNTS YOU
|
|
270
|
+
PAID SHAKACODE FOR THE SOFTWARE IN THE TWELVE MONTHS BEFORE THE CLAIM
|
|
271
|
+
AROSE.
|
|
272
|
+
|
|
273
|
+
## 15. Assignment, governing law, and miscellaneous
|
|
274
|
+
|
|
275
|
+
You may assign these terms, together with any active Subscription, to a
|
|
276
|
+
successor in connection with a merger, acquisition, or sale of
|
|
277
|
+
substantially all assets, upon notice to ShakaCode; these terms are
|
|
278
|
+
otherwise non-transferable. These terms are governed by the laws of the
|
|
279
|
+
State of Hawaii, USA, excluding its conflict-of-law rules, with exclusive
|
|
280
|
+
jurisdiction and venue in the state and federal courts located in
|
|
281
|
+
Honolulu, Hawaii. If a provision of these terms is unenforceable, the
|
|
282
|
+
remainder stays in effect. These terms, together with any subscription
|
|
283
|
+
agreement or order, are the entire agreement about the software; if they
|
|
284
|
+
conflict, the order controls.
|
|
285
|
+
|
|
286
|
+
## 16. Questions and commercial licensing
|
|
287
|
+
|
|
288
|
+
contact@shakacode.com · https://pro.reactonrails.com/ ·
|
|
289
|
+
https://github.com/shakacode/commercial-licensing
|
|
290
|
+
|
|
291
|
+
## Machine-readable summary (informative)
|
|
292
|
+
|
|
293
|
+
```yaml
|
|
294
|
+
license_name: The Inertia Rails SSR Cache License 1.0
|
|
295
|
+
spdx_identifier: LicenseRef-InertiaRailsSsrCache
|
|
296
|
+
type: trust-based commercial
|
|
297
|
+
free_production_use_if_all:
|
|
298
|
+
max_org_headcount_fte: 10 # exclusive; whole control group
|
|
299
|
+
max_ttm_revenue_usd: 1000000 # exclusive; trailing 12 months
|
|
300
|
+
max_lifetime_outside_capital_usd: 1000000 # exclusive
|
|
301
|
+
free_production_use_if_any:
|
|
302
|
+
- charity # recognized charitable / public-benefit org, any size
|
|
303
|
+
- educational_institution # school, college, university, any size
|
|
304
|
+
- hospital # licensed inpatient care provider, any size
|
|
305
|
+
always_free: [read, study, development, test, ci, staging, preview, review_apps, evaluate_in_production_45_days, education, personal, oss_projects]
|
|
306
|
+
paid: production use by any other organization; included in an active ShakaStack Pro subscription for organizations under 250 people and under USD 10M revenue; otherwise a separate written agreement
|
|
307
|
+
license_key: none
|
|
308
|
+
runtime_enforcement: none # no activation, no telemetry, no lockout
|
|
309
|
+
attribution_required: false
|
|
310
|
+
subscription_lapse_grace_days: 30
|
|
311
|
+
outgrow_free_line_grace_days: 60
|
|
312
|
+
per_version_terms: true
|
|
313
|
+
pricing_url: https://pro.reactonrails.com/
|
|
314
|
+
contact: contact@shakacode.com
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Version history (informative)
|
|
318
|
+
|
|
319
|
+
Version 1.0 is the first license under which the software is distributed.
|
data/README.md
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# Inertia Rails SSR Cache
|
|
2
|
+
|
|
3
|
+
Warm the server-side rendering (SSR) cache of an Inertia Rails app after each
|
|
4
|
+
deploy, so the first visitors to your public pages get cached SSR HTML instead of
|
|
5
|
+
waiting for Node to render it.
|
|
6
|
+
|
|
7
|
+
You list the public paths to warm. One Active Job job requests each path the way an
|
|
8
|
+
anonymous visitor would and checks that a second, fresh request reuses the same
|
|
9
|
+
cache entry Inertia wrote. Inertia keeps owning page serialization, cache keys and
|
|
10
|
+
cache writes; this gem only drives and verifies them. A small npm companion,
|
|
11
|
+
`guardRelease`, wraps your Node SSR callback so a page from one release is never
|
|
12
|
+
rendered by another release's bundle.
|
|
13
|
+
|
|
14
|
+
Version 0.1.0 is an early release. Run the
|
|
15
|
+
[adopter pilot](docs/releasing.md#adopter-pilot-before-production-use) with your
|
|
16
|
+
own routes, cache topology and load before enabling warming in production.
|
|
17
|
+
|
|
18
|
+
Works with any Active Job backend; there is no Sidekiq requirement. GQL LRU warming
|
|
19
|
+
is out of scope.
|
|
20
|
+
|
|
21
|
+
## What is tested
|
|
22
|
+
|
|
23
|
+
- Ruby 3.3+, Rails 7.2–8.1 and `inertia_rails` 3.22.x.
|
|
24
|
+
- The Rails integration suite runs against Inertia React 3.9.1, including reuse
|
|
25
|
+
of a FileStore cache from a separate web process.
|
|
26
|
+
- The `guardRelease` callback type is checked against the `createServer`
|
|
27
|
+
callbacks of Inertia React, Vue 3 and Svelte 3.9.1. A Node test renders a Vue 3
|
|
28
|
+
page through the guard. Svelte is checked for types only.
|
|
29
|
+
- Node 22.18+; the npm package is ESM only.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Gemfile
|
|
35
|
+
gem "inertia_rails_ssr_cache", "~> 0.1.0"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
bundle install
|
|
40
|
+
npm install inertia-rails-ssr-cache@0.1.0
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install the same version of both packages.
|
|
44
|
+
|
|
45
|
+
## Application setup
|
|
46
|
+
|
|
47
|
+
Configure Inertia normally. Use an immutable release token embedded in the SSR
|
|
48
|
+
bundle and a retaining cache store shared by warming workers and web processes.
|
|
49
|
+
Warming requires `config.enable_reloading = false` and
|
|
50
|
+
`config.allow_concurrency` must not be `false`. Enqueueing and execution reject
|
|
51
|
+
these unsupported settings: nested requests can deadlock against the surrounding
|
|
52
|
+
job's Rails reload or concurrency locks.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
# config/initializers/inertia.rb
|
|
56
|
+
InertiaRails.configure do |config|
|
|
57
|
+
config.ssr_enabled = true
|
|
58
|
+
config.ssr_cache = { expires_in: 1.hour }
|
|
59
|
+
config.version = ENV.fetch("APP_RELEASE")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# config/initializers/inertia_rails_ssr_cache.rb
|
|
63
|
+
InertiaRailsSsrCache.configure do |config|
|
|
64
|
+
config.origin = "https://www.example.com"
|
|
65
|
+
config.paths = ["/", "/pricing", "/features"]
|
|
66
|
+
config.queue_name = :ssr_warming
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Wrap your existing Node SSR callback:
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { guardRelease } from 'inertia-rails-ssr-cache';
|
|
74
|
+
import createServer from '@inertiajs/react/server';
|
|
75
|
+
|
|
76
|
+
// Replace with a literal or bundler-defined value embedded during the build.
|
|
77
|
+
createServer(guardRelease('build-42', renderPage));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The embedded value must equal Rails' `APP_RELEASE`. A runtime environment value
|
|
81
|
+
alone cannot identify which JavaScript was built. Deploy with a fresh version
|
|
82
|
+
when first adding the guard; cache hits do not execute the callback, so it cannot
|
|
83
|
+
certify old entries written by an unguarded renderer.
|
|
84
|
+
|
|
85
|
+
After the candidate Rails workers, SSR service and database are ready:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
job_id = InertiaRailsSsrCache.warm_later
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Or enqueue through the Rails task:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
bin/rails inertia_rails_ssr_cache:warm
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The returned job ID confirms enqueueing, not completion. Monitor normal Active
|
|
98
|
+
Job failures and `target.inertia_rails_ssr_cache` / `warm.inertia_rails_ssr_cache`
|
|
99
|
+
notifications. Target results report a reason such as `reused`,
|
|
100
|
+
`missing_cache_evidence`, `cache_key_changed`, `cache_not_reused`, `http_failure`,
|
|
101
|
+
`release_mismatch` or `invalid_ssr_value`. A failed run raises
|
|
102
|
+
`InertiaRailsSsrCache::WarmFailed` with its report. Events contain target/result
|
|
103
|
+
metadata, not page props or HTML.
|
|
104
|
+
|
|
105
|
+
## What verification means
|
|
106
|
+
|
|
107
|
+
Each job processes at most 50 distinct registered paths sequentially. Each path
|
|
108
|
+
gets two fresh anonymous HTML GETs through the full Rails middleware stack,
|
|
109
|
+
without credentials, cookie carry-over, Inertia visit headers or redirect
|
|
110
|
+
following. The synthetic Rack environment uses source address `192.0.2.1` and no
|
|
111
|
+
User-Agent header. Both responses must be HTTP 200 HTML. Each must produce exactly one
|
|
112
|
+
native SSR cache-fetch observation, and the second must hit the first one's key.
|
|
113
|
+
The retained entry must also contain a string body and an array of string head
|
|
114
|
+
entries; cached null/false values do not qualify.
|
|
115
|
+
|
|
116
|
+
This catches normal client-rendering fallback, disabled/nonretaining caches and
|
|
117
|
+
unstable page data. Success describes observed reuse at that moment; eviction,
|
|
118
|
+
expiry and future request variants can still cause misses. The application must
|
|
119
|
+
approve safe public GETs and deterministic anonymous props that do not vary by
|
|
120
|
+
client IP, geolocation or browser identity. Context-dependent
|
|
121
|
+
Inertia versions are unsupported: worker and controller versions must agree.
|
|
122
|
+
|
|
123
|
+
A native SSR hit avoids Node rendering. Controllers, queries and prop resolution
|
|
124
|
+
still execute. This package makes no database-cache or production latency claim.
|
|
125
|
+
|
|
126
|
+
## Operational limits
|
|
127
|
+
|
|
128
|
+
- Concurrency is one request per job. Multiple jobs can overlap; configure a
|
|
129
|
+
dedicated queue's concurrency in your Active Job backend.
|
|
130
|
+
- Each request uses a fresh joined thread to preserve the surrounding job's
|
|
131
|
+
Rails executor and CurrentAttributes. Allow for an additional database
|
|
132
|
+
connection if job callbacks retain one while waiting for the request.
|
|
133
|
+
- Route count is bounded, but execution has no hard wall-clock deadline.
|
|
134
|
+
Inertia Rails 3.22 uses its HTTP client's timeouts. The warmer does not interrupt
|
|
135
|
+
Rails requests with asynchronous Ruby timeouts. If the host interrupts a job,
|
|
136
|
+
it waits for its active request to finish before unwinding; shutdown can be
|
|
137
|
+
delayed by the remaining request duration.
|
|
138
|
+
- Readiness, migrations, replica lag, cutover and cancellation remain deployment
|
|
139
|
+
responsibilities. No scheduler, distributed lock or durable run registry is
|
|
140
|
+
installed. Retrying repeats eligible GETs and reuses entries already present.
|
|
141
|
+
- Queued jobs carry their intended release and recheck it at execution. Rollbacks
|
|
142
|
+
use the matching guarded bundle and immutable Inertia version.
|
|
143
|
+
- Worker and web cache topology must match. MemoryStore only warms one process;
|
|
144
|
+
the integration suite proves reuse with FileStore across separate processes.
|
|
145
|
+
- Native cache instrumentation compatibility is intentionally limited to the
|
|
146
|
+
tested Inertia Rails 3.22 line. A future adapter upgrade needs contract tests.
|
|
147
|
+
- Cache namespaces must be absent or static strings. Callable namespaces and
|
|
148
|
+
custom key encodings cannot be verified. FileStore support covers the built-in
|
|
149
|
+
class with keys that fit in one encoded filename. Longer keys and subclasses
|
|
150
|
+
are unsupported and can fail verification even if native caching works.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
Ruby 3.3+, Rails 7.2–8.1, `inertia_rails` 3.22.x, Node 22.18+ and the pnpm version
|
|
155
|
+
pinned in `package.json`. The Rails integration fixture and demo use Inertia React
|
|
156
|
+
3.9.1. The npm guard is also type-checked against Inertia Vue 3 and Svelte 3.9.1,
|
|
157
|
+
and renders a Vue 3 page in a Node test; Svelte has no runtime test.
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
bin/setup
|
|
161
|
+
bin/validate
|
|
162
|
+
bin/audit
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`bin/validate` runs behavior, type, lint, workflow and package checks. `bin/audit`
|
|
166
|
+
checks dependency advisories and requires network access. See the manifests and
|
|
167
|
+
[contribution guide](CONTRIBUTING.md) for the exact tools and commands.
|
|
168
|
+
|
|
169
|
+
## Runnable demo
|
|
170
|
+
|
|
171
|
+
Run `bin/demo` after `bin/setup` to start a small Inertia Rails app with React
|
|
172
|
+
SSR, a Solid Queue Active Job worker and a shared FileStore cache. Run
|
|
173
|
+
`bin/demo verify` for an isolated proof that warming completes and a separate
|
|
174
|
+
web process reuses the same native SSR entry. See the
|
|
175
|
+
[demo guide](examples/demo/README.md) for commands, configuration and limits.
|
|
176
|
+
|
|
177
|
+
See [architecture](docs/architecture.md), [source research](docs/grounding.md),
|
|
178
|
+
[design comparison](docs/design-comparison.md), the [changelog](CHANGELOG.md) and
|
|
179
|
+
the [release procedure](docs/releasing.md).
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
Inertia Rails SSR Cache uses ShakaCode Trust-Based Commercial Licensing, under
|
|
184
|
+
[The Inertia Rails SSR Cache License](LICENSE.md).
|
|
185
|
+
|
|
186
|
+
TL;DR: development, test, CI, staging and preview use is free for
|
|
187
|
+
everyone. Production use is free for organizations under 10 people, $1M
|
|
188
|
+
revenue and $1M raised, and for charities, schools and hospitals at any
|
|
189
|
+
size. Otherwise, an active ShakaStack Pro subscription (which includes
|
|
190
|
+
React on Rails Pro) covers it for organizations under 250 people and
|
|
191
|
+
$10M revenue; see [pro.reactonrails.com](https://pro.reactonrails.com/).
|
|
192
|
+
Larger organizations and questions:
|
|
193
|
+
[contact@shakacode.com](mailto:contact@shakacode.com).
|
|
194
|
+
|
|
195
|
+
There are no license keys and nothing phones home. [LICENSE.md](LICENSE.md) is the
|
|
196
|
+
binding text.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
require "uri"
|
|
11
|
+
|
|
12
|
+
module InertiaRailsSsrCache
|
|
13
|
+
class ConfigurationError < StandardError; end
|
|
14
|
+
|
|
15
|
+
# Boot-time registration is the only authority for eligible anonymous requests.
|
|
16
|
+
class Configuration
|
|
17
|
+
MAX_TARGETS = 50
|
|
18
|
+
RELEASE = /\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}\z/
|
|
19
|
+
INVALID_CHARACTERS = /[\\\x00-\x20\x7f]/
|
|
20
|
+
|
|
21
|
+
attr_accessor :origin, :paths, :queue_name
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@origin = nil
|
|
25
|
+
@paths = []
|
|
26
|
+
@queue_name = :ssr_warming
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate!
|
|
30
|
+
@origin = self.class.validate_origin(origin).dup.freeze
|
|
31
|
+
@paths = self.class.validate_paths(paths)
|
|
32
|
+
@queue_name = self.class.validate_queue_name(queue_name)
|
|
33
|
+
freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def verify_targets!(requested_origin, requested_paths)
|
|
37
|
+
checked_origin = self.class.validate_origin(requested_origin)
|
|
38
|
+
checked_paths = self.class.validate_paths(requested_paths)
|
|
39
|
+
unless checked_origin == origin && checked_paths.all? { |path| paths.include?(path) }
|
|
40
|
+
raise ConfigurationError, "queued targets are no longer registered"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
checked_paths
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_release(value)
|
|
47
|
+
unless value.is_a?(String) && RELEASE.match?(value)
|
|
48
|
+
raise ConfigurationError,
|
|
49
|
+
"release must be 1-128 ASCII letters, digits, dots, underscores or hyphens, starting alphanumeric"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
value
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.validate_queue_name(value)
|
|
56
|
+
unless (value.is_a?(String) || value.is_a?(Symbol)) && !value.to_s.strip.empty?
|
|
57
|
+
raise ConfigurationError, "queue_name must be a nonempty string or symbol"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
value.to_s.dup.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.valid_origin?(parsed)
|
|
64
|
+
parsed.is_a?(URI::HTTP) && !parsed.host.to_s.empty? && parsed.path == "" &&
|
|
65
|
+
[parsed.userinfo, parsed.query, parsed.fragment].all?(&:nil?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.validate_origin(value)
|
|
69
|
+
unless value.is_a?(String) && !INVALID_CHARACTERS.match?(value)
|
|
70
|
+
raise ConfigurationError, "origin must be an absolute HTTP(S) origin"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
parsed = URI.parse(value)
|
|
74
|
+
unless valid_origin?(parsed)
|
|
75
|
+
raise ConfigurationError, "origin must contain only HTTP(S) scheme and host, with an optional port"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
value
|
|
79
|
+
rescue URI::InvalidURIError
|
|
80
|
+
raise ConfigurationError, "origin is not a valid URI"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.validate_paths(value)
|
|
84
|
+
unless value.is_a?(Array) && !value.empty? && value.all?(String)
|
|
85
|
+
raise ConfigurationError, "paths must be a nonempty array of rooted paths"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
paths = value.uniq
|
|
89
|
+
if paths.size > MAX_TARGETS
|
|
90
|
+
raise ConfigurationError,
|
|
91
|
+
"at most #{MAX_TARGETS} distinct paths may be warmed per job"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
paths.each { |path| validate_path(path) }
|
|
95
|
+
paths.map { |path| path.dup.freeze }.freeze
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def self.validate_path(path)
|
|
99
|
+
unless path.start_with?("/") && !path.start_with?("//") && !INVALID_CHARACTERS.match?(path)
|
|
100
|
+
raise ConfigurationError, "each path must be rooted and must not contain an authority or control characters"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
parsed = URI.parse(path)
|
|
104
|
+
unless [parsed.fragment, parsed.host, parsed.scheme].all?(&:nil?)
|
|
105
|
+
raise ConfigurationError, "paths must not contain fragments, hosts or schemes"
|
|
106
|
+
end
|
|
107
|
+
rescue URI::InvalidURIError
|
|
108
|
+
raise ConfigurationError, "path is not a valid URI"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
require "rack/mock"
|
|
11
|
+
require "active_support/dependencies"
|
|
12
|
+
require "active_support/notifications"
|
|
13
|
+
|
|
14
|
+
module InertiaRailsSsrCache
|
|
15
|
+
# The native-key format belongs only here; compatibility is pinned to Inertia Rails 3.22.
|
|
16
|
+
class Probe
|
|
17
|
+
Fetch = Data.define(:key, :native_key, :hit, :namespace, :version)
|
|
18
|
+
NATIVE_KEY = %r{\Ainertia_ssr/[0-9a-f]{32}\z}
|
|
19
|
+
|
|
20
|
+
def self.call(origin:, path:)
|
|
21
|
+
Thread.handle_interrupt(Object => :never) { wait_for_request(start_request(origin, path)) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.start_request(origin, path)
|
|
25
|
+
Thread.new do
|
|
26
|
+
Thread.current.report_on_exception = false
|
|
27
|
+
Thread.handle_interrupt(Object => :immediate) { new(origin, path).call }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.wait_for_request(request_thread)
|
|
32
|
+
# Rails request middleware resets its executor; isolate it from the job's executor.
|
|
33
|
+
Thread.handle_interrupt(Object => :immediate) do
|
|
34
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads { request_thread.value }
|
|
35
|
+
end
|
|
36
|
+
ensure
|
|
37
|
+
drain_request(request_thread)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.drain_request(request_thread)
|
|
41
|
+
# Never interrupt Rails completion callbacks or replace an interrupted parent's error.
|
|
42
|
+
# The normal wait above already propagates request exceptions.
|
|
43
|
+
ActiveSupport::Dependencies.interlock.permit_concurrent_loads { request_thread.join }
|
|
44
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
45
|
+
raise if request_thread.alive?
|
|
46
|
+
end
|
|
47
|
+
private_class_method :start_request, :wait_for_request, :drain_request
|
|
48
|
+
|
|
49
|
+
def initialize(origin, path)
|
|
50
|
+
@origin = origin
|
|
51
|
+
@path = path
|
|
52
|
+
@fetches = []
|
|
53
|
+
@owner_thread = Thread.current
|
|
54
|
+
@owner_fiber = Fiber.current
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def call
|
|
58
|
+
response = ActiveSupport::Notifications.subscribed(method(:record), "cache_read.active_support") { request }
|
|
59
|
+
Observation.new(status: response.status, html: response.media_type == "text/html",
|
|
60
|
+
fetches: @fetches.map { |fetch| [fetch.key, fetch.hit] }, ssr: retained_ssr?)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def request
|
|
66
|
+
Rack::MockRequest.new(Rails.application).get(
|
|
67
|
+
"#{@origin}#{@path}",
|
|
68
|
+
"HTTP_HOST" => @origin.sub(%r{\Ahttps?://}i, ""),
|
|
69
|
+
"HTTP_ACCEPT" => "text/html",
|
|
70
|
+
"REMOTE_ADDR" => "192.0.2.1"
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def record(*)
|
|
75
|
+
return unless Thread.current.equal?(@owner_thread) && Fiber.current.equal?(@owner_fiber)
|
|
76
|
+
|
|
77
|
+
payload = ActiveSupport::Notifications::Event.new(*).payload
|
|
78
|
+
return unless payload[:super_operation] == :fetch
|
|
79
|
+
|
|
80
|
+
native_key = native_key(payload)
|
|
81
|
+
return unless native_key
|
|
82
|
+
|
|
83
|
+
@fetches << Fetch.new(payload[:key], native_key, payload[:hit] == true, payload[:namespace], payload[:version])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def native_key(payload)
|
|
87
|
+
logical_key = logical_key(payload[:key], payload[:store])
|
|
88
|
+
return unless logical_key
|
|
89
|
+
|
|
90
|
+
namespace = payload[:namespace]
|
|
91
|
+
return unless namespace.nil? || namespace.is_a?(String)
|
|
92
|
+
|
|
93
|
+
logical_key = logical_key.delete_prefix("#{namespace}:") if namespace
|
|
94
|
+
logical_key if NATIVE_KEY.match?(logical_key)
|
|
95
|
+
rescue Encoding::CompatibilityError
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def retained_ssr?
|
|
100
|
+
return false unless @fetches.size == 1
|
|
101
|
+
|
|
102
|
+
fetch = @fetches.fetch(0)
|
|
103
|
+
value = InertiaRails.cache_store.read(fetch.native_key, namespace: fetch.namespace, version: fetch.version)
|
|
104
|
+
Observation.ssr_value?(value)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def logical_key(key, store)
|
|
108
|
+
return unless key.is_a?(String)
|
|
109
|
+
|
|
110
|
+
decoded = if store == "ActiveSupport::Cache::FileStore"
|
|
111
|
+
URI.decode_www_form_component(File.basename(key))
|
|
112
|
+
else
|
|
113
|
+
key
|
|
114
|
+
end
|
|
115
|
+
decoded if decoded.valid_encoding?
|
|
116
|
+
rescue ArgumentError
|
|
117
|
+
# Unparseable notification keys must not interrupt unrelated cache requests.
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
module InertiaRailsSsrCache
|
|
11
|
+
class Railtie < Rails::Railtie
|
|
12
|
+
rake_tasks do
|
|
13
|
+
namespace :inertia_rails_ssr_cache do
|
|
14
|
+
desc "Enqueue native SSR cache warming for registered public routes"
|
|
15
|
+
task warm: :environment do
|
|
16
|
+
puts "Enqueued SSR warming job #{InertiaRailsSsrCache.warm_later}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
module InertiaRailsSsrCache
|
|
11
|
+
# Evidence interpretation is pure; framework events are reduced before this boundary.
|
|
12
|
+
class Observation
|
|
13
|
+
attr_reader :status, :html, :fetches, :ssr
|
|
14
|
+
|
|
15
|
+
def initialize(status:, html:, fetches:, ssr:)
|
|
16
|
+
@ssr = ssr
|
|
17
|
+
@status = status
|
|
18
|
+
@html = html
|
|
19
|
+
@fetches = fetches.freeze
|
|
20
|
+
freeze
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.ssr_value?(value)
|
|
24
|
+
return false unless value.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
head = value["head"]
|
|
27
|
+
value["body"].is_a?(String) && head.is_a?(Array) && head.all?(String)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def failure
|
|
31
|
+
return :http_failure unless status == 200 && html
|
|
32
|
+
return :missing_cache_evidence if fetches.empty?
|
|
33
|
+
return :ambiguous_cache_evidence unless fetches.size == 1
|
|
34
|
+
return :invalid_ssr_value unless ssr
|
|
35
|
+
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class TargetResult
|
|
41
|
+
attr_reader :path, :outcome, :initial_cache_hit
|
|
42
|
+
|
|
43
|
+
def initialize(path:, outcome:, initial_cache_hit: nil)
|
|
44
|
+
@path = path.dup.freeze
|
|
45
|
+
@outcome = outcome
|
|
46
|
+
@initial_cache_hit = initial_cache_hit
|
|
47
|
+
freeze
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.compare(path, first, second)
|
|
51
|
+
failure = first.failure || second.failure
|
|
52
|
+
return new(path: path, outcome: failure) if failure
|
|
53
|
+
|
|
54
|
+
initial_key, initial_hit = first.fetches.fetch(0)
|
|
55
|
+
repeat_key, repeat_hit = second.fetches.fetch(0)
|
|
56
|
+
outcome = reuse_outcome(initial_key, repeat_key, repeat_hit)
|
|
57
|
+
new(path: path, outcome: outcome, initial_cache_hit: initial_hit)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.reuse_outcome(initial_key, repeat_key, repeat_hit)
|
|
61
|
+
return :cache_key_changed if initial_key != repeat_key
|
|
62
|
+
return :cache_not_reused unless repeat_hit
|
|
63
|
+
|
|
64
|
+
:reused
|
|
65
|
+
end
|
|
66
|
+
private_class_method :reuse_outcome
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class Report
|
|
70
|
+
attr_reader :release, :results
|
|
71
|
+
|
|
72
|
+
def initialize(release:, results:)
|
|
73
|
+
@release = release.dup.freeze
|
|
74
|
+
@results = results.freeze
|
|
75
|
+
freeze
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def success?
|
|
79
|
+
!results.empty? && results.all? { |result| result.outcome == :reused }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class WarmFailed < StandardError
|
|
84
|
+
attr_reader :report
|
|
85
|
+
|
|
86
|
+
def initialize(report)
|
|
87
|
+
@report = report
|
|
88
|
+
outcomes = report.results.reject { |result| result.outcome == :reused }.map(&:outcome).uniq
|
|
89
|
+
super("SSR warming could not verify cache reuse: #{outcomes.join(', ')}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
module InertiaRailsSsrCache
|
|
11
|
+
VERSION = "0.1.0"
|
|
12
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
require "active_job"
|
|
11
|
+
|
|
12
|
+
module InertiaRailsSsrCache
|
|
13
|
+
# Backend configuration owns fleet concurrency. This job never fans out or retries itself.
|
|
14
|
+
class WarmJob < ActiveJob::Base
|
|
15
|
+
def self.validate_environment!
|
|
16
|
+
config = Rails.application.config
|
|
17
|
+
if config.reloading_enabled?
|
|
18
|
+
raise ConfigurationError, "SSR cache warming requires config.enable_reloading = false in the job process"
|
|
19
|
+
end
|
|
20
|
+
return unless config.allow_concurrency == false
|
|
21
|
+
|
|
22
|
+
raise ConfigurationError, "SSR cache warming requires config.allow_concurrency to permit concurrent execution"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def perform(origin, paths, release)
|
|
26
|
+
self.class.validate_environment!
|
|
27
|
+
registered = InertiaRailsSsrCache.configuration
|
|
28
|
+
targets = registered.verify_targets!(origin, paths)
|
|
29
|
+
InertiaRailsSsrCache.validate_release!(release)
|
|
30
|
+
results = targets.map { |path| warm_target(origin, path, release) }
|
|
31
|
+
report = Report.new(release: release, results: results)
|
|
32
|
+
ActiveSupport::Notifications.instrument("warm.inertia_rails_ssr_cache", report: report)
|
|
33
|
+
raise WarmFailed, report unless report.success?
|
|
34
|
+
|
|
35
|
+
report
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def warm_target(origin, path, release)
|
|
41
|
+
result = if InertiaRailsSsrCache.current_release == release
|
|
42
|
+
first = Probe.call(origin: origin, path: path)
|
|
43
|
+
second = Probe.call(origin: origin, path: path)
|
|
44
|
+
TargetResult.compare(path, first, second)
|
|
45
|
+
else
|
|
46
|
+
TargetResult.new(path: path, outcome: :release_mismatch)
|
|
47
|
+
end
|
|
48
|
+
ActiveSupport::Notifications.instrument("target.inertia_rails_ssr_cache", result: result, release: release)
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
6
|
+
#
|
|
7
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
8
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
9
|
+
|
|
10
|
+
require "rails"
|
|
11
|
+
require "inertia_rails"
|
|
12
|
+
require_relative "inertia_rails_ssr_cache/version"
|
|
13
|
+
require_relative "inertia_rails_ssr_cache/configuration"
|
|
14
|
+
require_relative "inertia_rails_ssr_cache/result"
|
|
15
|
+
require_relative "inertia_rails_ssr_cache/probe"
|
|
16
|
+
require_relative "inertia_rails_ssr_cache/railtie"
|
|
17
|
+
|
|
18
|
+
module InertiaRailsSsrCache
|
|
19
|
+
autoload :WarmJob, "#{__dir__}/inertia_rails_ssr_cache/warm_job"
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def configure
|
|
23
|
+
configuration = Configuration.new
|
|
24
|
+
yield configuration
|
|
25
|
+
@configuration = configuration.validate!
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def configuration
|
|
29
|
+
@configuration || raise(ConfigurationError, "register public warming targets with configure first")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def current_release
|
|
33
|
+
validate_release!(InertiaRails.configuration.version)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def validate_release!(release)
|
|
37
|
+
Configuration.validate_release(release)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def warm_later
|
|
41
|
+
registered = configuration
|
|
42
|
+
release = current_release.dup.freeze
|
|
43
|
+
WarmJob.validate_environment!
|
|
44
|
+
job = WarmJob.set(queue: registered.queue_name).perform_later(registered.origin, registered.paths, release)
|
|
45
|
+
raise ConfigurationError, "Active Job did not enqueue the warming job" unless job
|
|
46
|
+
|
|
47
|
+
job.job_id
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
4
|
+
#
|
|
5
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
6
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
7
|
+
|
|
8
|
+
# These narrow interfaces describe only the framework calls used by the public facade.
|
|
9
|
+
# Inertia's evaluated version can be arbitrary application data; validation narrows it.
|
|
10
|
+
interface _InertiaVersionConfiguration
|
|
11
|
+
def version: () -> Object?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module InertiaRails
|
|
15
|
+
def self.configuration: () -> _InertiaVersionConfiguration
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
interface _EnqueuedWarmJob
|
|
19
|
+
def job_id: () -> String
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
interface _ConfiguredWarmJob
|
|
23
|
+
def perform_later: (String? origin, Array[String] paths, String release) -> (_EnqueuedWarmJob | false)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module InertiaRailsSsrCache
|
|
27
|
+
class WarmJob
|
|
28
|
+
def self.validate_environment!: () -> void
|
|
29
|
+
def self.set: (queue: String | Symbol) -> _ConfiguredWarmJob
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright (c) 2026 ShakaCode LLC.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LicenseRef-InertiaRailsSsrCache
|
|
4
|
+
#
|
|
5
|
+
# This file is part of Inertia Rails SSR Cache. Use is governed by The
|
|
6
|
+
# Inertia Rails SSR Cache License in LICENSE.md.
|
|
7
|
+
|
|
8
|
+
module InertiaRailsSsrCache
|
|
9
|
+
self.@configuration: Configuration?
|
|
10
|
+
def self.configure: () { (Configuration) -> void } -> Configuration
|
|
11
|
+
def self.configuration: () -> Configuration
|
|
12
|
+
def self.current_release: () -> String
|
|
13
|
+
def self.validate_release!: (Object? release) -> String
|
|
14
|
+
def self.warm_later: () -> String
|
|
15
|
+
class ConfigurationError < StandardError
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Configuration
|
|
19
|
+
MAX_TARGETS: Integer
|
|
20
|
+
RELEASE: Regexp
|
|
21
|
+
INVALID_CHARACTERS: Regexp
|
|
22
|
+
attr_accessor origin: String?
|
|
23
|
+
attr_accessor paths: Array[String]
|
|
24
|
+
attr_accessor queue_name: String | Symbol
|
|
25
|
+
|
|
26
|
+
def initialize: () -> void
|
|
27
|
+
def validate!: () -> self
|
|
28
|
+
def verify_targets!: (String requested_origin, Array[String] requested_paths) -> Array[String]
|
|
29
|
+
def self.validate_release: (Object? value) -> String
|
|
30
|
+
def self.validate_queue_name: (String | Symbol value) -> String
|
|
31
|
+
def self.valid_origin?: (URI::Generic parsed) -> bool
|
|
32
|
+
def self.validate_origin: (String? value) -> String
|
|
33
|
+
def self.validate_paths: (Array[String] value) -> Array[String]
|
|
34
|
+
def self.validate_path: (String path) -> void
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
type outcome = :reused | :http_failure | :missing_cache_evidence | :ambiguous_cache_evidence | :invalid_ssr_value | :cache_key_changed | :cache_not_reused | :release_mismatch
|
|
38
|
+
type cache_fetch = [String, bool]
|
|
39
|
+
|
|
40
|
+
class Observation
|
|
41
|
+
attr_reader ssr: bool
|
|
42
|
+
attr_reader status: Integer
|
|
43
|
+
attr_reader html: bool
|
|
44
|
+
attr_reader fetches: Array[cache_fetch]
|
|
45
|
+
def initialize: (status: Integer, html: bool, fetches: Array[cache_fetch], ssr: bool) -> void
|
|
46
|
+
def self.ssr_value?: (Object? value) -> bool
|
|
47
|
+
def failure: () -> (:http_failure | :missing_cache_evidence | :ambiguous_cache_evidence | :invalid_ssr_value)?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class TargetResult
|
|
51
|
+
attr_reader path: String
|
|
52
|
+
attr_reader outcome: outcome
|
|
53
|
+
attr_reader initial_cache_hit: bool?
|
|
54
|
+
def initialize: (path: String, outcome: outcome, ?initial_cache_hit: bool?) -> void
|
|
55
|
+
def self.reuse_outcome: (String initial_key, String repeat_key, bool repeat_hit) -> outcome
|
|
56
|
+
def self.compare: (String path, Observation first, Observation second) -> TargetResult
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class Report
|
|
60
|
+
attr_reader release: String
|
|
61
|
+
attr_reader results: Array[TargetResult]
|
|
62
|
+
def initialize: (release: String, results: Array[TargetResult]) -> void
|
|
63
|
+
def success?: () -> bool
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class WarmFailed < StandardError
|
|
67
|
+
attr_reader report: Report
|
|
68
|
+
def initialize: (Report report) -> void
|
|
69
|
+
end
|
|
70
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inertia_rails_ssr_cache
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Justin Gordon
|
|
8
|
+
- ShakaCode
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: actionpack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.2'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '8.2'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '7.2'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '8.2'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: activejob
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.2'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '8.2'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '7.2'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '8.2'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: activesupport
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '7.2'
|
|
60
|
+
- - "<"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '8.2'
|
|
63
|
+
type: :runtime
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '7.2'
|
|
70
|
+
- - "<"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '8.2'
|
|
73
|
+
- !ruby/object:Gem::Dependency
|
|
74
|
+
name: inertia_rails
|
|
75
|
+
requirement: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.22.0
|
|
80
|
+
type: :runtime
|
|
81
|
+
prerelease: false
|
|
82
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
83
|
+
requirements:
|
|
84
|
+
- - "~>"
|
|
85
|
+
- !ruby/object:Gem::Version
|
|
86
|
+
version: 3.22.0
|
|
87
|
+
- !ruby/object:Gem::Dependency
|
|
88
|
+
name: rack
|
|
89
|
+
requirement: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.1'
|
|
94
|
+
- - "<"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '4'
|
|
97
|
+
type: :runtime
|
|
98
|
+
prerelease: false
|
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.1'
|
|
104
|
+
- - "<"
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '4'
|
|
107
|
+
- !ruby/object:Gem::Dependency
|
|
108
|
+
name: railties
|
|
109
|
+
requirement: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '7.2'
|
|
114
|
+
- - "<"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '8.2'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '7.2'
|
|
124
|
+
- - "<"
|
|
125
|
+
- !ruby/object:Gem::Version
|
|
126
|
+
version: '8.2'
|
|
127
|
+
description: Warm explicitly registered public routes through the native Inertia SSR
|
|
128
|
+
cache.
|
|
129
|
+
email:
|
|
130
|
+
- justin@shakacode.com
|
|
131
|
+
executables: []
|
|
132
|
+
extensions: []
|
|
133
|
+
extra_rdoc_files: []
|
|
134
|
+
files:
|
|
135
|
+
- LICENSE.md
|
|
136
|
+
- README.md
|
|
137
|
+
- lib/inertia_rails_ssr_cache.rb
|
|
138
|
+
- lib/inertia_rails_ssr_cache/configuration.rb
|
|
139
|
+
- lib/inertia_rails_ssr_cache/probe.rb
|
|
140
|
+
- lib/inertia_rails_ssr_cache/railtie.rb
|
|
141
|
+
- lib/inertia_rails_ssr_cache/result.rb
|
|
142
|
+
- lib/inertia_rails_ssr_cache/version.rb
|
|
143
|
+
- lib/inertia_rails_ssr_cache/warm_job.rb
|
|
144
|
+
- sig/framework_boundary.rbs
|
|
145
|
+
- sig/inertia_rails_ssr_cache.rbs
|
|
146
|
+
homepage: https://github.com/shakacode/inertia_rails_ssr_cache
|
|
147
|
+
licenses:
|
|
148
|
+
- LicenseRef-InertiaRailsSsrCache
|
|
149
|
+
metadata:
|
|
150
|
+
allowed_push_host: https://rubygems.org
|
|
151
|
+
rubygems_mfa_required: 'true'
|
|
152
|
+
source_code_uri: https://github.com/shakacode/inertia_rails_ssr_cache
|
|
153
|
+
documentation_uri: https://github.com/shakacode/inertia_rails_ssr_cache#readme
|
|
154
|
+
changelog_uri: https://github.com/shakacode/inertia_rails_ssr_cache/blob/main/CHANGELOG.md
|
|
155
|
+
bug_tracker_uri: https://github.com/shakacode/inertia_rails_ssr_cache/issues
|
|
156
|
+
license_uri: https://github.com/shakacode/inertia_rails_ssr_cache/blob/main/LICENSE.md
|
|
157
|
+
rdoc_options: []
|
|
158
|
+
require_paths:
|
|
159
|
+
- lib
|
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
161
|
+
requirements:
|
|
162
|
+
- - ">="
|
|
163
|
+
- !ruby/object:Gem::Version
|
|
164
|
+
version: '3.3'
|
|
165
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
166
|
+
requirements:
|
|
167
|
+
- - ">="
|
|
168
|
+
- !ruby/object:Gem::Version
|
|
169
|
+
version: '0'
|
|
170
|
+
requirements: []
|
|
171
|
+
rubygems_version: 4.0.18
|
|
172
|
+
specification_version: 4
|
|
173
|
+
summary: Verified Inertia Rails SSR cache warming through Active Job.
|
|
174
|
+
test_files: []
|