coldwire-rails 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +28 -26
- data/VERSION +1 -1
- data/app/assets/javascripts/coldwire/cache_controller.js +116 -4
- data/app/assets/javascripts/coldwire/entries.js +15 -0
- data/app/assets/javascripts/coldwire/format.js +11 -1
- data/app/helpers/coldwire/service_worker_helper.rb +5 -0
- data/app/views/coldwire/caches/show.html.erb +47 -0
- data/app/views/coldwire/service_worker/show.js.erb +10 -1
- data/docs/README.md +3 -2
- data/docs/configuration.md +185 -31
- data/docs/how-it-works.md +1 -1
- data/docs/images/coldwire-logo-dark.svg +57 -0
- data/docs/images/coldwire-logo.svg +57 -0
- data/docs/images/coldwire-mark-dark.svg +26 -0
- data/docs/images/coldwire-mark.svg +26 -0
- data/docs/images/noreaster-group.png +0 -0
- data/docs/images/offline-settings.png +0 -0
- data/docs/setup.md +13 -8
- data/lib/coldwire/client/collect.js +49 -0
- data/lib/coldwire/client/store.js +23 -1
- data/lib/coldwire/configuration.rb +121 -17
- data/lib/coldwire/debug.css +7 -0
- data/lib/coldwire/source.rb +1 -1
- data/lib/coldwire/worker/collect.js +153 -0
- data/lib/coldwire/worker/events.js +5 -0
- data/lib/coldwire/worker/rules.js +4 -4
- data/lib/coldwire/worker/serve.js +7 -4
- data/lib/generators/coldwire/install/install_generator.rb +1 -1
- data/lib/generators/coldwire/install/templates/coldwire.rb +24 -7
- metadata +9 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e48a15a424535143cc620dbbe65eec4a367c9e1feaa60addc104587ee835c836
|
|
4
|
+
data.tar.gz: b85fc7cfc8b80db94b14e994afbb258cc997d0b161986738e80c33a8a49704d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 543c39e2d9999a45ce8041d4c87cb2853908056611bdc189d49e0fde87309373260131f433f92715d29943fcd44eb1d429aa6f270ef639287c0d45ce86787f11
|
|
7
|
+
data.tar.gz: 31bc9a01f008bd987c79700058b49682cae9d1020e873bf9aab03949fe9d15d9619c420c7b5c9efe7032a6b07eddaa090b54d43adecd9cf2f503983d4e702632
|
data/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
- **`cache_origins` is `cacheable_hosts`**, and takes bare hosts: `"tiles.example.com"` rather
|
|
8
|
+
than `"https://tiles.example.com"`. The scheme was never carrying information — a worker
|
|
9
|
+
runs only on a secure page, and a secure page cannot fetch `http` — so it was a required
|
|
10
|
+
prefix with exactly one possible value. Requests are matched on a URL's `host`, so a port
|
|
11
|
+
belongs where it is not the default and `localhost:3001` matches that port and no other.
|
|
12
|
+
A scheme raises at boot, naming what to write instead.
|
|
13
|
+
|
|
14
|
+
## [0.2.0]
|
|
15
|
+
|
|
16
|
+
- **Garbage collection** is on by default. `config.garbage_collection` periodically sweeps the cache, deleting entries that haven't been accessed for `max_age` (default: 60 days), and if the cache grows over `max_size` (default: 250 MB), it continues pruning the least recently accessed until the cache fits. Sweeps run only when confirmed online by pinging `probe_path`, since deletions are irreversible. Cached archives and offline page assets are never collected, and the age of an entry is renewed anytime it or its referenced subresources are accessed or stored. The size ceiling can be adjusted in the offline settings page and is remembered per device.
|
|
17
|
+
- The garbage collector runs safely and automatically; you generally do not need to configure it. But you can tune `max_age`, `max_size`, and `interval` to fit your app's needs.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
## [0.1.0]
|
|
21
|
+
|
|
5
22
|
First release. The API may still change before 1.0.
|
|
6
23
|
|
|
7
24
|
- **`bin/rails coldwire:install`.** Mounts the engine at `/offline`, writes an initializer
|
|
8
25
|
with every option and its default, registers the Stimulus controller, and tags the
|
|
9
26
|
layout. Safe to run twice.
|
|
27
|
+
- **Garbage collection.** `config.garbage_collection` sweeps entries nothing has used in
|
|
28
|
+
`max_age` (60 days by default) and, once the cache is over `max_size` (250 MB by default),
|
|
29
|
+
the least recently read of what is left until it fits — so a cache that fills as people
|
|
30
|
+
browse does not fill forever, on a device that revisits nothing as much as on one that
|
|
31
|
+
revisits everything. The ceiling is offered as a ladder of sizes on the offline settings
|
|
32
|
+
page and remembered per device, since how much of a phone to spend is not something an app
|
|
33
|
+
can know. It measures only what a sweep may take: downloaded archives are an opt-in spend
|
|
34
|
+
of somebody's data plan, so they are neither counted nor evicted.
|
|
35
|
+
On by default, unlike syncing: it spends no data. A sweep runs only with a
|
|
36
|
+
connection it has confirmed by pinging `probe_path`, because deleting is the one cache
|
|
37
|
+
operation with no way back. Age is measured from last use, not from when an entry was
|
|
38
|
+
fetched — storing a page renews everything it names, so the stylesheet every page loads
|
|
39
|
+
keeps a fresh date even though nothing ever refetches it. The offline page's own assets and
|
|
40
|
+
downloaded archives are never collected.
|
|
10
41
|
- **Offline support switch** in the status header on the offline settings page. Off deletes
|
|
11
42
|
what is stored, unregisters the worker, and hides the rest of the page.
|
|
12
43
|
`config.caching_enabled_by_default` is the starting position of the switch (on). A device
|
data/README.md
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
<picture>
|
|
2
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/coldwire-logo-dark.svg">
|
|
3
|
+
<img src="docs/images/coldwire-logo.svg" alt="Coldwire" width="380">
|
|
4
|
+
</picture>
|
|
2
5
|
|
|
3
6
|
**When your Hotwire wires go cold.**
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
people browse, and falls back to a view [Turbo](https://turbo.hotwired.dev) will actually
|
|
8
|
-
render.
|
|
8
|
+
[](https://rubygems.org/gems/coldwire-rails)
|
|
9
|
+
[](https://rubygems.org/gems/coldwire-rails)
|
|
9
10
|
|
|
10
|
-
Works the same in a plain Hotwire app, an installed PWA, or Hotwire Native.
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
Offline caching for Rails, extracted from a production app. Add a gem, mount it, drop a
|
|
13
|
+
tag in your layout — pages start working without a network. A service worker precaches
|
|
14
|
+
what you nominate, keeps pages as people browse, and falls back to a view
|
|
15
|
+
[Turbo](https://turbo.hotwired.dev) will actually render.
|
|
16
|
+
|
|
17
|
+
Works the same in a plain web app, an installed PWA, or Hotwire Native.
|
|
14
18
|
|
|
15
19
|
> [!TIP]
|
|
20
|
+
> <a href="https://noreastergroup.com"><img src="docs/images/noreaster-group.png" alt="Noreaster Group" width="220"></a>
|
|
21
|
+
>
|
|
16
22
|
> **Need help going offline?** Coldwire is built by [Noreaster Group](https://noreastergroup.com).
|
|
17
23
|
> If you want a hand adding offline to your Hotwire or Hotwire Native app,
|
|
18
24
|
> [talk to us](https://noreastergroup.com).
|
|
19
25
|
|
|
20
26
|
## What you get
|
|
21
27
|
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
- **
|
|
27
|
-
- **Offline settings
|
|
28
|
-
- **
|
|
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>
|
|
28
|
+
- **[Setup in 30 seconds](docs/setup.md):** Instal gem, then `bin/rails coldwire:install`. The rest has working defaults.
|
|
29
|
+
- **[Cache as you go](docs/configuration.md#cache_as_you_go):** Defaults to all pages get cached as your navigate, customize to only cache certain parts of the app.
|
|
30
|
+
- **[Auto Sycn precaching](docs/configuration.md#auto_sync):** Define urls that can be pre-cached automatically in the background.
|
|
31
|
+
- **[Garbage collection](docs/configuration.md#garbage_collection):** Entries nothing has used in two months are swept, and anything over the storage ceiling goes least recently read first — so the cache doesn't grow forever. Only with a connection; anything a page still loads is renewed on the way past. People can set the ceiling for their own device on `/offline`.
|
|
32
|
+
- **[Offline fallback page](docs/setup.md#the-offline-page):** A customizable page when a user is offline and no page is cached for that given url.
|
|
33
|
+
- **[Offline settings page](docs/setup.md#the-offline-settings-page):** at `/offline`: turn offline support off, force offline, sync, downloads, inspect what's cached.
|
|
34
|
+
- **[Easy Tailwind Variants](docs/configuration.md#tailwind-variants):** Easily show or hide content with an `online:` or `offline:` Tailwind variant.
|
|
36
35
|
|
|
37
36
|
<p align="center">
|
|
38
|
-
<img src="docs/images/offline-
|
|
37
|
+
<img src="docs/images/offline-settings.png" alt="Offline settings: status, force offline, the storage limit, auto sync, and downloads" width="240">
|
|
38
|
+
<img src="docs/images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="240">
|
|
39
|
+
<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="240">
|
|
39
40
|
</p>
|
|
40
41
|
|
|
41
|
-
<p align="center"><em>
|
|
42
|
+
<p align="center"><em>The offline settings page (Inspect cache is the URL list), and the fallback when a page isn't cached.</em></p>
|
|
42
43
|
|
|
43
44
|
## Quick start
|
|
44
45
|
|
|
@@ -55,8 +56,9 @@ That mounts the engine at `/offline`, writes `config/initializers/coldwire.rb`,
|
|
|
55
56
|
the Stimulus controller, and adds the tag to your layout. Visit `/offline` to see what's
|
|
56
57
|
cached.
|
|
57
58
|
|
|
58
|
-
For signed-in apps
|
|
59
|
-
turn on `auto_sync`. Both are in the
|
|
59
|
+
For signed-in apps that do not use `current_user` or `Current.user`, set `cache_identity`.
|
|
60
|
+
To have pages ready before anyone visits them, turn on `auto_sync`. Both are in the
|
|
61
|
+
[configuration reference](docs/configuration.md).
|
|
60
62
|
|
|
61
63
|
## Docs
|
|
62
64
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.0
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import { formatBytes, formatCachedAt, formatDuration, formatInterval, displayUrl, plural } from "coldwire/format"
|
|
2
|
+
import { formatBytes, formatLimit, formatCachedAt, formatDuration, formatInterval, displayUrl, plural } from "coldwire/format"
|
|
3
3
|
import { sendToWorker } from "coldwire/worker"
|
|
4
4
|
import { renderArchiveStatus, renderArchiveProgress, toggleArchiveBusy } from "coldwire/archives"
|
|
5
|
-
import { describeEntry, describeCached, describeFinishedSync } from "coldwire/entries"
|
|
5
|
+
import { describeEntry, describeCached, describeFinishedSync, isDownload } from "coldwire/entries"
|
|
6
6
|
|
|
7
7
|
// Storage goes through the one wrapper the head snippet defines, so the page and the snippet
|
|
8
8
|
// cannot disagree about where anything is kept. Without that snippet there is no service
|
|
@@ -15,7 +15,8 @@ const INERT_STORE = {
|
|
|
15
15
|
number: () => 0,
|
|
16
16
|
on: () => false,
|
|
17
17
|
toggle: () => {},
|
|
18
|
-
cachingOn: () => true
|
|
18
|
+
cachingOn: () => true,
|
|
19
|
+
maxSize: () => null
|
|
19
20
|
}
|
|
20
21
|
const SYNC_MESSAGE = "coldwire:sync"
|
|
21
22
|
|
|
@@ -47,6 +48,10 @@ export default class extends Controller {
|
|
|
47
48
|
"search",
|
|
48
49
|
"sort",
|
|
49
50
|
"inspect",
|
|
51
|
+
"maxSize",
|
|
52
|
+
"usage",
|
|
53
|
+
"usageBar",
|
|
54
|
+
"usageLabel",
|
|
50
55
|
"forgetTemplate",
|
|
51
56
|
"detail",
|
|
52
57
|
"detailUrl",
|
|
@@ -79,6 +84,7 @@ export default class extends Controller {
|
|
|
79
84
|
this.restoreInspect()
|
|
80
85
|
this.restoreForced()
|
|
81
86
|
this.restoreAutoSync()
|
|
87
|
+
this.restoreMaxSize()
|
|
82
88
|
if (this.cachingOn()) {
|
|
83
89
|
this.renderArchives()
|
|
84
90
|
this.refresh()
|
|
@@ -746,11 +752,12 @@ export default class extends Controller {
|
|
|
746
752
|
(cache.entries || []).map((entry) => {
|
|
747
753
|
const path = displayUrl(entry.url)
|
|
748
754
|
|
|
749
|
-
return { ...entry, cache: cache.name, path, search: path.toLowerCase() }
|
|
755
|
+
return { ...entry, cache: cache.name, path, search: path.toLowerCase(), download: isDownload(entry.url) }
|
|
750
756
|
}))
|
|
751
757
|
this.cacheCount = cachesInfo.length
|
|
752
758
|
|
|
753
759
|
this.renderEntries()
|
|
760
|
+
this.renderStorage()
|
|
754
761
|
}
|
|
755
762
|
|
|
756
763
|
filterEntries() {
|
|
@@ -792,6 +799,111 @@ export default class extends Controller {
|
|
|
792
799
|
return entries.reduce((sum, entry) => sum + (entry.size || 0), 0)
|
|
793
800
|
}
|
|
794
801
|
|
|
802
|
+
// MARK: the storage ceiling
|
|
803
|
+
|
|
804
|
+
// How much the cache may grow to before a sweep starts taking the oldest back, chosen here
|
|
805
|
+
// and remembered on the device. The app sets the starting position; this is the person
|
|
806
|
+
// holding the phone deciding how much of it to spend.
|
|
807
|
+
restoreMaxSize() {
|
|
808
|
+
if (!this.hasMaxSizeTarget) return
|
|
809
|
+
|
|
810
|
+
const limit = this.store.maxSize()
|
|
811
|
+
const value = limit === null ? "none" : String(limit)
|
|
812
|
+
const options = [ ...this.maxSizeTarget.options ]
|
|
813
|
+
|
|
814
|
+
// A ceiling this device chose before the app changed its own is no longer on the menu.
|
|
815
|
+
// Showing a blank select instead would misreport the setting that is actually in force,
|
|
816
|
+
// so it joins the list — in order, and labelled from the same figures as the rest.
|
|
817
|
+
if (!options.some((option) => option.value === value)) {
|
|
818
|
+
const option = document.createElement("option")
|
|
819
|
+
option.value = value
|
|
820
|
+
option.textContent = formatLimit(limit)
|
|
821
|
+
const after = options.find((candidate) => Number(candidate.value) > limit)
|
|
822
|
+
this.maxSizeTarget.insertBefore(option, after || null)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
this.maxSizeTarget.value = value
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
async changeMaxSize(event) {
|
|
829
|
+
const select = event.currentTarget
|
|
830
|
+
this.store.set(this.store.keys.maxSize, select.value)
|
|
831
|
+
this.renderStorage()
|
|
832
|
+
|
|
833
|
+
// Held while the sweep runs. It is usually a moment, but picking a second size over a
|
|
834
|
+
// sweep still measuring the cache would leave two runs racing to a different ceiling.
|
|
835
|
+
select.disabled = true
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
// A lowered ceiling that waits for the next scheduled sweep reads as a setting that did
|
|
839
|
+
// nothing. This is that same sweep, run now — and bound by the same rule, so with no
|
|
840
|
+
// connection it stands down and the line under the bar says why.
|
|
841
|
+
await this.collectNow()
|
|
842
|
+
} finally {
|
|
843
|
+
select.disabled = false
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
async collectNow() {
|
|
848
|
+
let result = null
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
result = await sendToWorker("collect", { maxSize: this.store.maxSize() }, 60000)
|
|
852
|
+
} catch {
|
|
853
|
+
// No worker controlling this page yet. The cache is unchanged and the line already
|
|
854
|
+
// says where it stands.
|
|
855
|
+
return
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Nothing was swept, so the clock stays where it is and the next page load finds the
|
|
859
|
+
// sweep still due — exactly as the head snippet treats a refused run.
|
|
860
|
+
this.sweepWaiting = Boolean(result?.offline)
|
|
861
|
+
if (!this.sweepWaiting) this.store.set(this.store.keys.collectedAt, Date.now())
|
|
862
|
+
|
|
863
|
+
await this.renderCache()
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// What the ceiling is measured against: everything but the downloads, which are an opt-in
|
|
867
|
+
// spend of somebody's data plan and are never collected however full the cache gets — a
|
|
868
|
+
// 300 MB archive counted here would show a bar pinned full of files no sweep can touch.
|
|
869
|
+
//
|
|
870
|
+
// The worker also spares the offline page's own assets, which this cannot pick out of a
|
|
871
|
+
// list of URLs. That is a handful of files, and erring towards the larger figure is the
|
|
872
|
+
// right way round for a number somebody is deciding a limit from.
|
|
873
|
+
managedBytes(entries) {
|
|
874
|
+
return this.totalBytes((entries || []).filter((entry) => !entry.download))
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
renderStorage() {
|
|
878
|
+
if (!this.hasUsageLabelTarget) return
|
|
879
|
+
|
|
880
|
+
const limit = this.store.maxSize()
|
|
881
|
+
const used = this.managedBytes(this.entries)
|
|
882
|
+
|
|
883
|
+
if (this.hasUsageTarget) this.usageTarget.hidden = limit === null
|
|
884
|
+
|
|
885
|
+
if (limit === null) {
|
|
886
|
+
this.usageLabelTarget.textContent = `${formatBytes(used)} of cached pages, with no limit set.`
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const percent = Math.round(Math.min(used / limit, 1) * 100)
|
|
891
|
+
if (this.hasUsageBarTarget) this.usageBarTarget.style.width = `${percent}%`
|
|
892
|
+
if (this.hasUsageTarget) this.usageTarget.setAttribute("aria-valuenow", String(percent))
|
|
893
|
+
|
|
894
|
+
const parts = [ `${formatBytes(used)} of ${formatLimit(limit)}` ]
|
|
895
|
+
if (used > limit) {
|
|
896
|
+
// Over the ceiling is not a fault and not a promise of instant deletion: a sweep needs
|
|
897
|
+
// a connection, and says so rather than leaving somebody watching a bar that will not
|
|
898
|
+
// move.
|
|
899
|
+
parts.push(this.sweepWaiting || this.online === false
|
|
900
|
+
? "over, waiting for a connection to sweep"
|
|
901
|
+
: "over, the oldest go on the next sweep")
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
this.usageLabelTarget.textContent = `${parts.join(" · ")}.`
|
|
905
|
+
}
|
|
906
|
+
|
|
795
907
|
renderEntries() {
|
|
796
908
|
if (!this.hasEntriesTarget) return
|
|
797
909
|
|
|
@@ -6,6 +6,21 @@ import { formatBytes, formatCachedAt } from "coldwire/format"
|
|
|
6
6
|
// of its own.
|
|
7
7
|
export const TIMESTAMP_HEADER = "timestamp"
|
|
8
8
|
|
|
9
|
+
// The query params the worker keys a downloaded archive and its byte ranges under. A download
|
|
10
|
+
// is a deliberate spend of somebody's data plan and is never collected, so anything measuring
|
|
11
|
+
// what a sweep can take has to leave it out.
|
|
12
|
+
const DOWNLOAD_PARAMS = [ "__coldwire_chunk", "__coldwire_range" ]
|
|
13
|
+
|
|
14
|
+
export function isDownload(href) {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(href)
|
|
17
|
+
|
|
18
|
+
return DOWNLOAD_PARAMS.some((param) => url.searchParams.has(param))
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
export function describeEntry(entry) {
|
|
10
25
|
const parts = [ formatBytes(entry.size) ]
|
|
11
26
|
if (entry.timestamp) parts.push(`cached ${formatCachedAt(entry.timestamp)}`)
|
|
@@ -37,7 +37,17 @@ export function formatBytes(bytes) {
|
|
|
37
37
|
const kb = bytes / 1024
|
|
38
38
|
return `${kb < 10 ? kb.toFixed(1) : Math.round(kb)} KB`
|
|
39
39
|
}
|
|
40
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
40
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
41
|
+
|
|
42
|
+
// A storage ceiling is the one figure here that reaches gigabytes, and "2048.0 MB" is a
|
|
43
|
+
// number somebody has to convert in their head before it means anything.
|
|
44
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(bytes % (1024 * 1024 * 1024) === 0 ? 0 : 1)} GB`
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A ceiling somebody picked off a menu should read back the way the menu wrote it. Beside a
|
|
48
|
+
// select saying "250 MB", a line saying "250.0 MB" reads as a different number.
|
|
49
|
+
export function formatLimit(bytes) {
|
|
50
|
+
return formatBytes(bytes).replace(".0 ", " ")
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
// Built once. Constructing an Intl formatter is expensive, and this is called for every row
|
|
@@ -34,6 +34,10 @@ module Coldwire
|
|
|
34
34
|
workerPath: coldwire.service_worker_path,
|
|
35
35
|
workerScope: Coldwire.config.worker_scope,
|
|
36
36
|
syncInterval: Coldwire.config.auto_sync.interval.to_i * 1000,
|
|
37
|
+
collectInterval: Coldwire.config.garbage_collection.interval.to_i * 1000,
|
|
38
|
+
# The app's ceiling, in bytes. A device that has chosen its own overrides it; without
|
|
39
|
+
# this the page would have nothing to fall back to when it has not.
|
|
40
|
+
maxSize: Coldwire.config.garbage_collection.max_size,
|
|
37
41
|
cachingEnabledByDefault: Coldwire.config.caching_enabled_by_default,
|
|
38
42
|
userAgentCookie: USER_AGENT_COOKIE
|
|
39
43
|
}
|
|
@@ -45,6 +49,7 @@ module Coldwire
|
|
|
45
49
|
parts << "marker" if Coldwire.config.mark_cached_pages
|
|
46
50
|
parts += %w[cookie forced]
|
|
47
51
|
parts << "sync" if Coldwire.config.auto_sync.enabled
|
|
52
|
+
parts << "collect" if Coldwire.config.garbage_collection.enabled
|
|
48
53
|
parts << "register"
|
|
49
54
|
end
|
|
50
55
|
|
|
@@ -106,6 +106,53 @@
|
|
|
106
106
|
</div>
|
|
107
107
|
|
|
108
108
|
<div data-coldwire-cache-target="whenOn">
|
|
109
|
+
<% gc = Coldwire.config.garbage_collection %>
|
|
110
|
+
<% if gc.enabled %>
|
|
111
|
+
<%# What the cache is allowed to grow to on this device. The choice is remembered here the
|
|
112
|
+
way Force offline and Sync on its own are — the app sets the starting point, the person
|
|
113
|
+
holding the phone decides how much of it to spend. %>
|
|
114
|
+
<% size_label = ->(bytes) do
|
|
115
|
+
gigabytes = bytes.to_f / (1024 * 1024 * 1024)
|
|
116
|
+
next "#{bytes / (1024 * 1024)} MB" if gigabytes < 1
|
|
117
|
+
"#{gigabytes == gigabytes.round ? gigabytes.round : gigabytes.round(1)} GB"
|
|
118
|
+
end %>
|
|
119
|
+
|
|
120
|
+
<div class="coldwire-card">
|
|
121
|
+
<h2>Storage</h2>
|
|
122
|
+
|
|
123
|
+
<label class="coldwire-choice">
|
|
124
|
+
<span>
|
|
125
|
+
<span class="coldwire-setting-title">Keep at most</span>
|
|
126
|
+
<span class="coldwire-setting-note">
|
|
127
|
+
Over this, the pages you have not opened in longest are deleted first.
|
|
128
|
+
</span>
|
|
129
|
+
</span>
|
|
130
|
+
<select class="coldwire-select"
|
|
131
|
+
aria-label="How much storage cached pages may use"
|
|
132
|
+
data-coldwire-cache-target="maxSize"
|
|
133
|
+
data-action="change->coldwire-cache#changeMaxSize">
|
|
134
|
+
<% gc.size_choices.each do |bytes| %>
|
|
135
|
+
<option value="<%= bytes %>"><%= size_label.call(bytes) %></option>
|
|
136
|
+
<% end %>
|
|
137
|
+
<option value="none">No limit</option>
|
|
138
|
+
</select>
|
|
139
|
+
</label>
|
|
140
|
+
|
|
141
|
+
<div style="margin-top: 0.9rem;"
|
|
142
|
+
role="progressbar"
|
|
143
|
+
aria-valuemin="0"
|
|
144
|
+
aria-valuemax="100"
|
|
145
|
+
data-coldwire-cache-target="usage"
|
|
146
|
+
hidden>
|
|
147
|
+
<div class="coldwire-track">
|
|
148
|
+
<div class="coldwire-bar" data-coldwire-cache-target="usageBar"></div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<p class="coldwire-facts" data-coldwire-cache-target="usageLabel">Reading the cache…</p>
|
|
153
|
+
</div>
|
|
154
|
+
<% end %>
|
|
155
|
+
|
|
109
156
|
<div class="coldwire-card">
|
|
110
157
|
<h2>Auto Sync</h2>
|
|
111
158
|
|
|
@@ -21,7 +21,7 @@ const CACHE_AS_YOU_GO = compileRules(<%= raw Coldwire.cache_rules(Coldwire.confi
|
|
|
21
21
|
const NEVER_CACHE = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.never_cache).to_json %>)
|
|
22
22
|
// Origins besides ours that may be cached at all, and the URLs whose byte ranges are cached
|
|
23
23
|
// piece by piece.
|
|
24
|
-
const
|
|
24
|
+
const CACHEABLE_HOSTS = <%= raw Coldwire.config.cacheable_hosts.to_json %>
|
|
25
25
|
const CACHE_RANGES = compileRules(<%= raw Coldwire.cache_rules(Coldwire.config.cache_ranges).to_json %>)
|
|
26
26
|
// A range entry is stored under its own key, with the range in the query and the archive's
|
|
27
27
|
// total size in a header — the two things needed to rebuild a 206 that was never storable.
|
|
@@ -44,6 +44,15 @@ const REFETCH_AFTER = <%= raw Coldwire.config.auto_sync.max_age ? Coldwire.confi
|
|
|
44
44
|
const SYNC_CONCURRENCY = <%= raw Coldwire.config.auto_sync.concurrency.to_i.to_json %>
|
|
45
45
|
// Marks entries the manifest owns, so a sync can retire the ones it dropped without
|
|
46
46
|
// touching assets or pages picked up by ordinary browsing.
|
|
47
|
+
// Collection. A null max age never collects and a null max size never evicts; the probe is
|
|
48
|
+
// how the worker tells a real connection from a hopeful one before it deletes anything. The
|
|
49
|
+
// ceiling here is the app's default — a device that has chosen its own sends that instead.
|
|
50
|
+
<%- gc = Coldwire.config.garbage_collection -%>
|
|
51
|
+
const COLLECT_MAX_AGE = <%= raw gc.enabled && gc.max_age ? gc.max_age.to_i.to_json : "null" %>
|
|
52
|
+
const COLLECT_MAX_SIZE = <%= raw gc.enabled && gc.max_size ? gc.max_size.to_i.to_json : "null" %>
|
|
53
|
+
const RENEW_AFTER = <%= raw gc.renew_after ? gc.renew_after.to_json : "null" %>
|
|
54
|
+
const PROBE_PATH = <%= raw Coldwire.config.probe_path.to_json %>
|
|
55
|
+
|
|
47
56
|
const MANAGED_HEADER = "coldwire-managed"
|
|
48
57
|
// One message type for the whole sync, carrying `state`. Two types would mean the page
|
|
49
58
|
// stamping the clock and the page drawing progress could drift apart.
|
data/docs/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Setup and configuration for [Coldwire](../README.md): the mountable Rails engine
|
|
|
4
4
|
caches pages for Hotwire, PWAs, and Hotwire Native.
|
|
5
5
|
|
|
6
6
|
<p align="center">
|
|
7
|
-
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, auto sync, and downloads" width="280">
|
|
7
|
+
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, the storage limit, auto sync, and downloads" width="280">
|
|
8
8
|
<img src="images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="280">
|
|
9
9
|
</p>
|
|
10
10
|
|
|
@@ -16,4 +16,5 @@ caches pages for Hotwire, PWAs, and Hotwire Native.
|
|
|
16
16
|
|
|
17
17
|
Everything is set in `config/initializers/coldwire.rb` through `Coldwire.configure`.
|
|
18
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`
|
|
19
|
+
needs your attention on a first install; `cache_identity` already uses `current_user` or
|
|
20
|
+
`Current.user` when either is in scope.
|