coldwire-rails 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/CHANGELOG.md +53 -0
- data/LICENSE +21 -0
- data/README.md +69 -0
- data/VERSION +1 -0
- data/app/assets/javascripts/coldwire/archives.js +52 -0
- data/app/assets/javascripts/coldwire/cache_controller.js +1058 -0
- data/app/assets/javascripts/coldwire/entries.js +55 -0
- data/app/assets/javascripts/coldwire/format.js +60 -0
- data/app/assets/javascripts/coldwire/worker.js +35 -0
- data/app/controllers/coldwire/application_controller.rb +21 -0
- data/app/controllers/coldwire/caches_controller.rb +33 -0
- data/app/controllers/coldwire/service_worker_controller.rb +26 -0
- data/app/helpers/coldwire/service_worker_helper.rb +59 -0
- data/app/views/coldwire/caches/show.html.erb +304 -0
- data/app/views/coldwire/service_worker/offline_frame.html.erb +16 -0
- data/app/views/coldwire/service_worker/offline_page.html.erb +112 -0
- data/app/views/coldwire/service_worker/show.js.erb +66 -0
- data/config/importmap.rb +7 -0
- data/config/routes.rb +8 -0
- data/docs/README.md +19 -0
- data/docs/configuration.md +539 -0
- data/docs/how-it-works.md +64 -0
- data/docs/images/offline-fallback.png +0 -0
- data/docs/images/offline-settings-cached.png +0 -0
- data/docs/images/offline-settings.png +0 -0
- data/docs/setup.md +218 -0
- data/lib/coldwire/client/api.js +44 -0
- data/lib/coldwire/client/cookie.js +13 -0
- data/lib/coldwire/client/forced.js +17 -0
- data/lib/coldwire/client/identity.js +28 -0
- data/lib/coldwire/client/marker.js +35 -0
- data/lib/coldwire/client/register.js +19 -0
- data/lib/coldwire/client/store.js +69 -0
- data/lib/coldwire/client/sync.js +122 -0
- data/lib/coldwire/client_user_agent.rb +48 -0
- data/lib/coldwire/configuration.rb +319 -0
- data/lib/coldwire/debug.css +197 -0
- data/lib/coldwire/engine.rb +34 -0
- data/lib/coldwire/source.rb +50 -0
- data/lib/coldwire/version.rb +11 -0
- data/lib/coldwire/worker/archives.js +158 -0
- data/lib/coldwire/worker/events.js +79 -0
- data/lib/coldwire/worker/inspect.js +81 -0
- data/lib/coldwire/worker/ranges.js +136 -0
- data/lib/coldwire/worker/rules.js +100 -0
- data/lib/coldwire/worker/serve.js +226 -0
- data/lib/coldwire/worker/sync.js +234 -0
- data/lib/coldwire-rails.rb +5 -0
- data/lib/coldwire.rb +55 -0
- data/lib/generators/coldwire/install/install_generator.rb +110 -0
- data/lib/generators/coldwire/install/templates/coldwire.rb +53 -0
- data/lib/tasks/coldwire.rake +8 -0
- metadata +113 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<%# Full-page offline fallback. Override by creating this same path in your own app.
|
|
2
|
+
|
|
3
|
+
Rendered when the worker script is built and embedded in it as a string, so this runs
|
|
4
|
+
without a real request: no current_user, no request-scoped helpers. %>
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html>
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<%# viewport-fit=cover is what makes env(safe-area-inset-*) resolve to anything. Without
|
|
10
|
+
it the layout viewport already sits inside the safe area and every inset reads 0, so
|
|
11
|
+
the padding below would silently do nothing. %>
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
13
|
+
<%# Served with a 200, so keep Turbo from snapshotting it and restoring it once back online. %>
|
|
14
|
+
<meta name="turbo-cache-control" content="no-cache">
|
|
15
|
+
<title>Offline</title>
|
|
16
|
+
|
|
17
|
+
<%# Hotwire Native reports "Turbo is not present" and shows its own error screen for any
|
|
18
|
+
page where window.Turbo never appears — a fallback without this is not renderable in
|
|
19
|
+
the app at all. Just Turbo, not the whole app entry point: every module in that graph
|
|
20
|
+
would have to be cached for it to evaluate. %>
|
|
21
|
+
<% if Coldwire.config.offline_import.present? && respond_to?(:javascript_inline_importmap_tag) %>
|
|
22
|
+
<%= javascript_inline_importmap_tag %>
|
|
23
|
+
<%= javascript_import_module_tag Coldwire.config.offline_import %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</head>
|
|
26
|
+
<body>
|
|
27
|
+
<%# Deliberately in the body, and every selector scoped to .coldwire-offline.
|
|
28
|
+
|
|
29
|
+
Turbo's head merge copies new <style> elements in and never takes them out again —
|
|
30
|
+
only metas and titles are removed and replaced. A <style> in the head would therefore
|
|
31
|
+
install itself into the app for the rest of the session, and a bare `body { padding }`
|
|
32
|
+
rule would push every later page around until a full reload cleared it. In the body it
|
|
33
|
+
goes away with the body swap, and the scoping means it does nothing even if it does
|
|
34
|
+
somehow outlive this page. %>
|
|
35
|
+
<style>
|
|
36
|
+
.coldwire-offline {
|
|
37
|
+
/* Fixed rather than a body rule: fills the viewport whether or not the host's CSS
|
|
38
|
+
resets body margins, and without styling body itself. */
|
|
39
|
+
position: fixed;
|
|
40
|
+
top: 0;
|
|
41
|
+
right: 0;
|
|
42
|
+
bottom: 0;
|
|
43
|
+
left: 0;
|
|
44
|
+
overflow-y: auto;
|
|
45
|
+
box-sizing: border-box;
|
|
46
|
+
color-scheme: light dark;
|
|
47
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
48
|
+
background: #ffffff;
|
|
49
|
+
color: #1f2937;
|
|
50
|
+
|
|
51
|
+
/* Hotwire Native apps often run the web view edge to edge, pinning it to the top
|
|
52
|
+
anchor rather than the safe area, so keep the content clear of the notch and the
|
|
53
|
+
home indicator. Additive, so there is 2rem of breathing room measured from the
|
|
54
|
+
safe edge rather than from the screen edge. */
|
|
55
|
+
padding:
|
|
56
|
+
calc(2rem + env(safe-area-inset-top, 0px))
|
|
57
|
+
calc(2rem + env(safe-area-inset-right, 0px))
|
|
58
|
+
calc(2rem + env(safe-area-inset-bottom, 0px))
|
|
59
|
+
calc(2rem + env(safe-area-inset-left, 0px));
|
|
60
|
+
}
|
|
61
|
+
/* State everything rather than leaning on UA defaults. This page carries its own
|
|
62
|
+
styles and nothing else, so anything left unsaid is simply missing. */
|
|
63
|
+
.coldwire-offline h1 { font-size: 1.5rem; font-weight: 700; line-height: 1.25; margin: 0 0 0.5rem; }
|
|
64
|
+
.coldwire-offline p { font-size: 1rem; line-height: 1.5; margin: 0; }
|
|
65
|
+
|
|
66
|
+
.coldwire-offline-button {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0.5rem;
|
|
70
|
+
margin-top: 1.5rem;
|
|
71
|
+
border: 1px solid #1f2937;
|
|
72
|
+
border-radius: 0.5rem;
|
|
73
|
+
background: #1f2937;
|
|
74
|
+
color: #ffffff;
|
|
75
|
+
padding: 0.625rem 1.25rem;
|
|
76
|
+
font-family: inherit;
|
|
77
|
+
font-size: 1rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
text-decoration: none;
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
.coldwire-offline-button svg {
|
|
84
|
+
width: 1.15rem;
|
|
85
|
+
height: 1.15rem;
|
|
86
|
+
flex-shrink: 0;
|
|
87
|
+
fill: none;
|
|
88
|
+
stroke: currentColor;
|
|
89
|
+
stroke-width: 1.5;
|
|
90
|
+
stroke-linecap: round;
|
|
91
|
+
stroke-linejoin: round;
|
|
92
|
+
}
|
|
93
|
+
</style>
|
|
94
|
+
|
|
95
|
+
<div class="coldwire-offline">
|
|
96
|
+
<h1>You're offline</h1>
|
|
97
|
+
<p>This page isn't available offline. Reconnect and try again.</p>
|
|
98
|
+
|
|
99
|
+
<%# An empty href resolves to whatever URL this page was served for, which is the one
|
|
100
|
+
thing this template cannot know — it is baked when the worker script is built.
|
|
101
|
+
data-turbo="false" forces a real navigation: a Turbo visit to the identical URL can
|
|
102
|
+
be treated as same-page and do nothing at all. %>
|
|
103
|
+
<a href="" data-turbo="false" class="coldwire-offline-button">
|
|
104
|
+
<svg viewBox="0 0 24 24" aria-hidden="true">
|
|
105
|
+
<path d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" />
|
|
106
|
+
</svg>
|
|
107
|
+
Try again
|
|
108
|
+
</a>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
</body>
|
|
112
|
+
</html>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Coldwire — offline page cache for Hotwire Native apps.
|
|
2
|
+
// Generated by the Coldwire engine; edit the gem, not this response.
|
|
3
|
+
//
|
|
4
|
+
// Built on the Cache API: https://developer.mozilla.org/en-US/docs/Web/API/Cache
|
|
5
|
+
//
|
|
6
|
+
// HTML visits recache through putFresh() even when a copy already exists — it retires the
|
|
7
|
+
// entry for that URL before writing, so one page is one entry however many Accepts asked for
|
|
8
|
+
// it — then fall back to cache.match() when offline. Assets stay cache-first. Precaching uses
|
|
9
|
+
// the same put/match path.
|
|
10
|
+
<%
|
|
11
|
+
offline_page = render(template: "coldwire/service_worker/offline_page", layout: false, formats: :html)
|
|
12
|
+
offline_frame = render(template: "coldwire/service_worker/offline_frame", layout: false, formats: :html)
|
|
13
|
+
%>
|
|
14
|
+
|
|
15
|
+
const CACHE_NAME = <%= raw Coldwire.config.cache_name.to_json %>
|
|
16
|
+
// Never intercepted: these go straight to the network, and fail outright when it is down.
|
|
17
|
+
const NEVER_INTERCEPT = <%= raw Coldwire.never_intercept(coldwire.service_worker_path, coldwire.pack_path, Coldwire.config.probe_path).to_json %>
|
|
18
|
+
// What browsing stores, and the one veto over storing anything at all. Neither governs the
|
|
19
|
+
// precache manifest, which is an explicit instruction.
|
|
20
|
+
const CACHE_AS_YOU_GO = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.cache_as_you_go).to_json %>)
|
|
21
|
+
const NEVER_CACHE = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.never_cache).to_json %>)
|
|
22
|
+
// Origins besides ours that may be cached at all, and the URLs whose byte ranges are cached
|
|
23
|
+
// piece by piece.
|
|
24
|
+
const CACHE_ORIGINS = <%= raw Coldwire.config.cache_origins.to_json %>
|
|
25
|
+
const CACHE_RANGES = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.cache_ranges).to_json %>)
|
|
26
|
+
// A range entry is stored under its own key, with the range in the query and the archive's
|
|
27
|
+
// total size in a header — the two things needed to rebuild a 206 that was never storable.
|
|
28
|
+
const RANGE_PARAM = "__coldwire_range"
|
|
29
|
+
// Archives offered for download in full, and the size of the pieces they arrive in. 8 MB is
|
|
30
|
+
// small enough that a dropped connection loses seconds rather than minutes, and large enough
|
|
31
|
+
// that a 300 MB archive is tens of requests rather than thousands.
|
|
32
|
+
const CACHE_ARCHIVES = <%= raw Coldwire.config.cache_archive_urls.to_json %>
|
|
33
|
+
const ARCHIVE_CHUNK = 8 * 1024 * 1024
|
|
34
|
+
const CHUNK_PARAM = "__coldwire_chunk"
|
|
35
|
+
const ARCHIVE_MESSAGE = "coldwire:archive"
|
|
36
|
+
const RANGE_TOTAL_HEADER = "coldwire-range-total"
|
|
37
|
+
const OFFLINE_PAGE = untrack(<%= raw offline_page.to_json %>)
|
|
38
|
+
const OFFLINE_FRAME_CONTENT = <%= raw offline_frame.to_json %>
|
|
39
|
+
const RETRY_URL_TOKEN = "coldwire:retry-url"
|
|
40
|
+
|
|
41
|
+
// Manifest sync.
|
|
42
|
+
const PACK_PATH = <%= raw coldwire.pack_path.to_json %>
|
|
43
|
+
const REFETCH_AFTER = <%= raw Coldwire.config.auto_sync.max_age ? Coldwire.config.auto_sync.max_age.to_i.to_json : "null" %>
|
|
44
|
+
const SYNC_CONCURRENCY = <%= raw Coldwire.config.auto_sync.concurrency.to_i.to_json %>
|
|
45
|
+
// Marks entries the manifest owns, so a sync can retire the ones it dropped without
|
|
46
|
+
// touching assets or pages picked up by ordinary browsing.
|
|
47
|
+
const MANAGED_HEADER = "coldwire-managed"
|
|
48
|
+
// One message type for the whole sync, carrying `state`. Two types would mean the page
|
|
49
|
+
// stamping the clock and the page drawing progress could drift apart.
|
|
50
|
+
const SYNC_MESSAGE = "coldwire:sync"
|
|
51
|
+
|
|
52
|
+
// Rails answers HTML with `Vary: Accept`, and cache.match() honors Vary by default.
|
|
53
|
+
// Precaching fetches with `Accept: */*` while Turbo navigations and frame loads send
|
|
54
|
+
// `Accept: text/html, application/xhtml+xml`, so a precached page would never match a real
|
|
55
|
+
// visit. ignoreVary makes matching URL-only, which is what a page cache wants.
|
|
56
|
+
const IGNORE_SEARCH = <%= raw Coldwire.config.ignore_query_params.to_json %>
|
|
57
|
+
const MATCH_OPTIONS = { ignoreVary: true, ignoreSearch: IGNORE_SEARCH }
|
|
58
|
+
const TIMESTAMP_HEADER = "timestamp"
|
|
59
|
+
const MARK_CACHED_PAGES = <%= raw Coldwire.config.mark_cached_pages.to_json %>
|
|
60
|
+
const OFFLINE_ATTRIBUTE = "data-coldwire-offline"
|
|
61
|
+
const CACHED_AT_ATTRIBUTE = "data-coldwire-cached-at"
|
|
62
|
+
|
|
63
|
+
let forcedOffline = false
|
|
64
|
+
let cachingEnabled = true
|
|
65
|
+
|
|
66
|
+
<%= raw Coldwire::Source.worker %>
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
pin "coldwire", to: "coldwire/cache_controller.js"
|
|
4
|
+
pin "coldwire/format", to: "coldwire/format.js"
|
|
5
|
+
pin "coldwire/worker", to: "coldwire/worker.js"
|
|
6
|
+
pin "coldwire/archives", to: "coldwire/archives.js"
|
|
7
|
+
pin "coldwire/entries", to: "coldwire/entries.js"
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Coldwire::Engine.routes.draw do
|
|
4
|
+
# `format: false` keeps the literal ".js" in the path instead of parsing it as a format.
|
|
5
|
+
get "service-worker.js", to: "service_worker#show", as: :service_worker, format: false
|
|
6
|
+
get "pack", to: "caches#pack", as: :pack
|
|
7
|
+
root "caches#show"
|
|
8
|
+
end
|
data/docs/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Coldwire docs
|
|
2
|
+
|
|
3
|
+
Setup and configuration for [Coldwire](../README.md): the mountable Rails engine that
|
|
4
|
+
caches pages for Hotwire, PWAs, and Hotwire Native.
|
|
5
|
+
|
|
6
|
+
<p align="center">
|
|
7
|
+
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, auto sync, and downloads" width="280">
|
|
8
|
+
<img src="images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="280">
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
| | |
|
|
12
|
+
|---|---|
|
|
13
|
+
| [Setup](setup.md) | Install the gem, mount the engine, register the controller, add the layout tag |
|
|
14
|
+
| [Configuration](configuration.md) | Every option, its default, and what changing it does |
|
|
15
|
+
| [How it works](how-it-works.md) | Why a naive cache fails in Hotwire, and what Coldwire does about it |
|
|
16
|
+
|
|
17
|
+
Everything is set in `config/initializers/coldwire.rb` through `Coldwire.configure`.
|
|
18
|
+
`bin/rails coldwire:install` writes that file with every default. Only `auto_sync` really
|
|
19
|
+
needs your attention on a first install; `cache_identity` needs it if anyone signs in.
|