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.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +53 -0
  3. data/LICENSE +21 -0
  4. data/README.md +69 -0
  5. data/VERSION +1 -0
  6. data/app/assets/javascripts/coldwire/archives.js +52 -0
  7. data/app/assets/javascripts/coldwire/cache_controller.js +1058 -0
  8. data/app/assets/javascripts/coldwire/entries.js +55 -0
  9. data/app/assets/javascripts/coldwire/format.js +60 -0
  10. data/app/assets/javascripts/coldwire/worker.js +35 -0
  11. data/app/controllers/coldwire/application_controller.rb +21 -0
  12. data/app/controllers/coldwire/caches_controller.rb +33 -0
  13. data/app/controllers/coldwire/service_worker_controller.rb +26 -0
  14. data/app/helpers/coldwire/service_worker_helper.rb +59 -0
  15. data/app/views/coldwire/caches/show.html.erb +304 -0
  16. data/app/views/coldwire/service_worker/offline_frame.html.erb +16 -0
  17. data/app/views/coldwire/service_worker/offline_page.html.erb +112 -0
  18. data/app/views/coldwire/service_worker/show.js.erb +66 -0
  19. data/config/importmap.rb +7 -0
  20. data/config/routes.rb +8 -0
  21. data/docs/README.md +19 -0
  22. data/docs/configuration.md +539 -0
  23. data/docs/how-it-works.md +64 -0
  24. data/docs/images/offline-fallback.png +0 -0
  25. data/docs/images/offline-settings-cached.png +0 -0
  26. data/docs/images/offline-settings.png +0 -0
  27. data/docs/setup.md +218 -0
  28. data/lib/coldwire/client/api.js +44 -0
  29. data/lib/coldwire/client/cookie.js +13 -0
  30. data/lib/coldwire/client/forced.js +17 -0
  31. data/lib/coldwire/client/identity.js +28 -0
  32. data/lib/coldwire/client/marker.js +35 -0
  33. data/lib/coldwire/client/register.js +19 -0
  34. data/lib/coldwire/client/store.js +69 -0
  35. data/lib/coldwire/client/sync.js +122 -0
  36. data/lib/coldwire/client_user_agent.rb +48 -0
  37. data/lib/coldwire/configuration.rb +319 -0
  38. data/lib/coldwire/debug.css +197 -0
  39. data/lib/coldwire/engine.rb +34 -0
  40. data/lib/coldwire/source.rb +50 -0
  41. data/lib/coldwire/version.rb +11 -0
  42. data/lib/coldwire/worker/archives.js +158 -0
  43. data/lib/coldwire/worker/events.js +79 -0
  44. data/lib/coldwire/worker/inspect.js +81 -0
  45. data/lib/coldwire/worker/ranges.js +136 -0
  46. data/lib/coldwire/worker/rules.js +100 -0
  47. data/lib/coldwire/worker/serve.js +226 -0
  48. data/lib/coldwire/worker/sync.js +234 -0
  49. data/lib/coldwire-rails.rb +5 -0
  50. data/lib/coldwire.rb +55 -0
  51. data/lib/generators/coldwire/install/install_generator.rb +110 -0
  52. data/lib/generators/coldwire/install/templates/coldwire.rb +53 -0
  53. data/lib/tasks/coldwire.rake +8 -0
  54. 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
+ }