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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f01aeddab9f97e903edaa61b6e17a674359cf508633835ed5df8d3fd77145fd8
|
|
4
|
+
data.tar.gz: e4b438c9a706473e652121ea5fd474057d5d93d42213a85846c28449e14e2f5e
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bd80ebb768d19c935f3426c7b48649c562ae8518f56c3539866ab579cc5eeee2caa04097c4c96144c8591c64a8c34857f966cef4dea4c8538b94a33c8a0d8a0f
|
|
7
|
+
data.tar.gz: 67b95dae87ed9c8bce9301911c85e6fdd26f7520836fcdcd73d47b9470424ae568c0ad1c1e4f4a9a8855e99c412d1f1321c4d71cab5ed7d0755a243e1242a581
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
First release. The API may still change before 1.0.
|
|
6
|
+
|
|
7
|
+
- **`bin/rails coldwire:install`.** Mounts the engine at `/offline`, writes an initializer
|
|
8
|
+
with every option and its default, registers the Stimulus controller, and tags the
|
|
9
|
+
layout. Safe to run twice.
|
|
10
|
+
- **Offline support switch** in the status header on the offline settings page. Off deletes
|
|
11
|
+
what is stored, unregisters the worker, and hides the rest of the page.
|
|
12
|
+
`config.caching_enabled_by_default` is the starting position of the switch (on). A device
|
|
13
|
+
remembers the choice.
|
|
14
|
+
- **Service worker and offline fallback.** A mountable engine serves the worker; when there is
|
|
15
|
+
no cached copy and no network, a full page or a `<turbo-frame>` — both overridable — stands
|
|
16
|
+
in. Built to satisfy Hotwire Native, which is stricter than a browser.
|
|
17
|
+
- **Precaching.** `auto_sync.precache_urls` is a list of URLs computed in Ruby, fetched along
|
|
18
|
+
with the subresources those pages reference.
|
|
19
|
+
- **The cache's requests carry the app's user agent**, by way of a cookie. A worker's own
|
|
20
|
+
fetches cannot: Hotwire Native sets the agent on the web view, `ServiceWorkerWebSettings` has
|
|
21
|
+
no equivalent, and `fetch` may not set one — so on Android everything precached came back
|
|
22
|
+
rendered for a browser. Each page writes its agent into `coldwire-user-agent`, which the
|
|
23
|
+
browser attaches to every same-origin request, and a middleware puts it back.
|
|
24
|
+
- **`cache_as_you_go`** names the pages kept as somebody browses — default `["/*"]`, every
|
|
25
|
+
path. An empty list stores nothing by browsing. **A stored page brings what it asks for**
|
|
26
|
+
— its stylesheets, scripts and images are stored with it whatever the lists say, because
|
|
27
|
+
a page held without them is the offline equivalent of not holding it. **`never_cache`**
|
|
28
|
+
is the one veto over storing anything, by any route in; it is not `never_intercept`,
|
|
29
|
+
which stops the worker touching a request at all and so fails outright offline.
|
|
30
|
+
- **Automatic syncing** on an interval, refetching anything older than `max_age`, resuming
|
|
31
|
+
across page loads when a run is cut short.
|
|
32
|
+
- **Offline settings page** at the mount point: connection status, force offline, an Auto Sync switch
|
|
33
|
+
with a countdown and live progress. The list of every cached URL is under **Inspect cache**,
|
|
34
|
+
closed until you open it and remembered after that.
|
|
35
|
+
- **Nothing served from the cache is `data-turbo-track="reload"`.** Turbo will not render a
|
|
36
|
+
page whose tracked elements differ from the current page's; it reloads instead, which
|
|
37
|
+
offline buys nothing and which Hotwire Native can hang on. Asset digests change with every
|
|
38
|
+
deploy, so any page cached before the current one was built disagrees with it — no
|
|
39
|
+
configuration could have reconciled that. This replaced the `offline_head` setting, whose
|
|
40
|
+
whole job was keeping the fallback's tracked elements in step with the layout by hand.
|
|
41
|
+
- **One cache entry per URL.** `cache.put()` replaces an entry only where the two agree about
|
|
42
|
+
`Vary`, and Rails answers HTML with `Vary: Accept` — so a page fetched by precaching
|
|
43
|
+
(`*/*`) and the same page visited by Turbo (`text/html`) are kept as two records, and the
|
|
44
|
+
list grows a copy per distinct Accept. Writing a page now retires the URL first. Range and
|
|
45
|
+
chunk entries, which share a path with their own query, are left alone.
|
|
46
|
+
- **Allow and block lists** written as route patterns (`"/sites/:id/card"`) or Regexps.
|
|
47
|
+
- **Cache identity.** The cache is dropped when the signed-in user changes.
|
|
48
|
+
- **Cross-origin caching** for origins you nominate, and **`Range` caching** for tiles and
|
|
49
|
+
media that would otherwise be uncacheable.
|
|
50
|
+
- **Downloadable archives.** Large files somebody can choose to keep, fetched in chunks so an
|
|
51
|
+
interrupted download resumes.
|
|
52
|
+
- **`window.Coldwire`** — `isOffline()`, `isForcedOffline()`, `cachedAt()`, `onChange()` — plus
|
|
53
|
+
`data-coldwire-offline` on any HTML served from cache.
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noreaster Group, LLC.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Coldwire
|
|
2
|
+
|
|
3
|
+
**When your Hotwire wires go cold.**
|
|
4
|
+
|
|
5
|
+
Offline caching for Rails. Add a gem, mount it, drop a tag in your layout — pages start
|
|
6
|
+
working without a network. A service worker precaches what you nominate, keeps pages as
|
|
7
|
+
people browse, and falls back to a view [Turbo](https://turbo.hotwired.dev) will actually
|
|
8
|
+
render.
|
|
9
|
+
|
|
10
|
+
Works the same in a plain Hotwire app, an installed PWA, or Hotwire Native.
|
|
11
|
+
|
|
12
|
+
> **Status: early.** Extracted from a production app but young as a library. The API may
|
|
13
|
+
> change before 1.0.
|
|
14
|
+
|
|
15
|
+
> [!TIP]
|
|
16
|
+
> **Need help going offline?** Coldwire is built by [Noreaster Group](https://noreastergroup.com).
|
|
17
|
+
> If you want a hand adding offline to your Hotwire or Hotwire Native app,
|
|
18
|
+
> [talk to us](https://noreastergroup.com).
|
|
19
|
+
|
|
20
|
+
## What you get
|
|
21
|
+
|
|
22
|
+
- **Minutes to wire.** `bin/rails coldwire:install`. The rest has working defaults.
|
|
23
|
+
- **Precaching in Ruby.** Nominate URLs with your own route helpers; assets come with them.
|
|
24
|
+
- **Cache as you go.** Pages someone visits are kept, with the styles, scripts, and images
|
|
25
|
+
they need to render.
|
|
26
|
+
- **An offline fallback** Turbo — and Hotwire Native — will actually show.
|
|
27
|
+
- **Offline settings** at `/offline`: turn offline support off, force offline, sync, downloads, what's cached.
|
|
28
|
+
- **Safe when people sign in.** The cache drops when the user changes.
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="docs/images/offline-settings.png" alt="Offline settings: status, force offline, auto sync, and downloads" width="280">
|
|
32
|
+
<img src="docs/images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="280">
|
|
33
|
+
</p>
|
|
34
|
+
|
|
35
|
+
<p align="center"><em>The offline settings page that ships with it. The URL list is under Inspect cache.</em></p>
|
|
36
|
+
|
|
37
|
+
<p align="center">
|
|
38
|
+
<img src="docs/images/offline-fallback.png" alt="The offline fallback: You're offline. This page isn't available offline. Reconnect and try again." width="280">
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<p align="center"><em>When a page isn't cached and there's no network, this is what people see — not a blank screen or a native error. Override it to match your app.</em></p>
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
# Gemfile
|
|
47
|
+
gem "coldwire-rails"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bin/rails coldwire:install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That mounts the engine at `/offline`, writes `config/initializers/coldwire.rb`, registers
|
|
55
|
+
the Stimulus controller, and adds the tag to your layout. Visit `/offline` to see what's
|
|
56
|
+
cached.
|
|
57
|
+
|
|
58
|
+
For signed-in apps, set `cache_identity`. To have pages ready before anyone visits them,
|
|
59
|
+
turn on `auto_sync`. Both are in the [configuration reference](docs/configuration.md).
|
|
60
|
+
|
|
61
|
+
## Docs
|
|
62
|
+
|
|
63
|
+
- [Setup](docs/setup.md) — install, requirements, Hotwire Native on iOS
|
|
64
|
+
- [Configuration](docs/configuration.md) — every option and what it does
|
|
65
|
+
- [How it works](docs/how-it-works.md) — why a naive cache fails in Hotwire
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT. See [LICENSE](LICENSE).
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// The download rows: what each one says, and what it looks like mid-download.
|
|
2
|
+
|
|
3
|
+
import { formatBytes, formatCachedAt } from "coldwire/format"
|
|
4
|
+
|
|
5
|
+
export function renderArchiveStatus(row, status) {
|
|
6
|
+
const label = row.querySelector("[data-archive-status]")
|
|
7
|
+
const download = row.querySelector("[data-archive-download-label]")
|
|
8
|
+
const remove = row.querySelector("[data-archive-remove]")
|
|
9
|
+
|
|
10
|
+
if (!status || !status.ok) {
|
|
11
|
+
label.textContent = "Not downloaded"
|
|
12
|
+
remove.hidden = true
|
|
13
|
+
download.textContent = "Download"
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
remove.hidden = status.chunks === 0
|
|
18
|
+
download.textContent = status.complete ? "Download again" : status.chunks ? "Resume" : "Download"
|
|
19
|
+
|
|
20
|
+
if (status.complete) {
|
|
21
|
+
label.textContent = status.cachedAt
|
|
22
|
+
? `${formatBytes(status.bytes)} · downloaded ${formatCachedAt(status.cachedAt)}`
|
|
23
|
+
: `${formatBytes(status.bytes)} · on this device`
|
|
24
|
+
} else if (status.chunks) {
|
|
25
|
+
// Partly downloaded is worth saying plainly: it is not broken, it stopped, and asking
|
|
26
|
+
// again carries on from there.
|
|
27
|
+
const share = status.expected ? Math.round((status.chunks / status.expected) * 100) : null
|
|
28
|
+
const stopped = status.cachedAt ? `, stopped ${formatCachedAt(status.cachedAt)}` : ""
|
|
29
|
+
label.textContent = share
|
|
30
|
+
? `${formatBytes(status.bytes)} of ${formatBytes(status.total)} · ${share}%${stopped}`
|
|
31
|
+
: `${formatBytes(status.bytes)} downloaded · not finished${stopped}`
|
|
32
|
+
} else {
|
|
33
|
+
label.textContent = "Not downloaded"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function renderArchiveProgress(row, done, total) {
|
|
37
|
+
const progress = row.querySelector("[data-archive-progress]")
|
|
38
|
+
const bar = row.querySelector("[data-archive-bar]")
|
|
39
|
+
const label = row.querySelector("[data-archive-progress-label]")
|
|
40
|
+
|
|
41
|
+
progress.hidden = false
|
|
42
|
+
const percent = total ? Math.round((done / total) * 100) : 0
|
|
43
|
+
bar.style.width = `${percent}%`
|
|
44
|
+
bar.classList.remove("coldwire-pulse")
|
|
45
|
+
progress.setAttribute("aria-valuenow", String(percent))
|
|
46
|
+
label.textContent = `${done} of ${total} pieces`
|
|
47
|
+
}
|
|
48
|
+
export function toggleArchiveBusy(row, busy) {
|
|
49
|
+
row.querySelector("[data-archive-spinner]").hidden = !busy
|
|
50
|
+
row.querySelectorAll("button").forEach((button) => { button.disabled = busy })
|
|
51
|
+
if (!busy) row.querySelector("[data-archive-progress]").hidden = true
|
|
52
|
+
}
|