coldwire-rails 0.1.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f01aeddab9f97e903edaa61b6e17a674359cf508633835ed5df8d3fd77145fd8
4
- data.tar.gz: e4b438c9a706473e652121ea5fd474057d5d93d42213a85846c28449e14e2f5e
3
+ metadata.gz: 268be717f39cc2dcb3927c27c22256afcc81981facc029f9be277ef9c22c1ee0
4
+ data.tar.gz: 03cc56cf120523536b582180dcb8366fc9194ca3ada5dca136abd44761cf7afb
5
5
  SHA512:
6
- metadata.gz: bd80ebb768d19c935f3426c7b48649c562ae8518f56c3539866ab579cc5eeee2caa04097c4c96144c8591c64a8c34857f966cef4dea4c8538b94a33c8a0d8a0f
7
- data.tar.gz: 67b95dae87ed9c8bce9301911c85e6fdd26f7520836fcdcd73d47b9470424ae568c0ad1c1e4f4a9a8855e99c412d1f1321c4d71cab5ed7d0755a243e1242a581
6
+ metadata.gz: d2bf7e4ef38d0b7f3e25483995663263468f092904810a9aa48186c0dff1abfd5429890ff0aadc628fd14730d287da327e05daac39d07d92f0c66507f10fc4a2
7
+ data.tar.gz: edea984f52957b16f2fd2f0664c9bcf262cf58735679b646b6c345730a4df65524f36e3a4fa3a8ff32ed17a697f0b030330ee670ac860a3fc0f3d06d6aefa120
data/CHANGELOG.md CHANGED
@@ -1,12 +1,26 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
3
+ ## [0.1.0]
4
4
 
5
5
  First release. The API may still change before 1.0.
6
6
 
7
7
  - **`bin/rails coldwire:install`.** Mounts the engine at `/offline`, writes an initializer
8
8
  with every option and its default, registers the Stimulus controller, and tags the
9
9
  layout. Safe to run twice.
10
+ - **Garbage collection.** `config.garbage_collection` sweeps entries nothing has used in
11
+ `max_age` (60 days by default) and, once the cache is over `max_size` (250 MB by default),
12
+ the least recently read of what is left until it fits — so a cache that fills as people
13
+ browse does not fill forever, on a device that revisits nothing as much as on one that
14
+ revisits everything. The ceiling is offered as a ladder of sizes on the offline settings
15
+ page and remembered per device, since how much of a phone to spend is not something an app
16
+ can know. It measures only what a sweep may take: downloaded archives are an opt-in spend
17
+ of somebody's data plan, so they are neither counted nor evicted.
18
+ On by default, unlike syncing: it spends no data. A sweep runs only with a
19
+ connection it has confirmed by pinging `probe_path`, because deleting is the one cache
20
+ operation with no way back. Age is measured from last use, not from when an entry was
21
+ fetched — storing a page renews everything it names, so the stylesheet every page loads
22
+ keeps a fresh date even though nothing ever refetches it. The offline page's own assets and
23
+ downloaded archives are never collected.
10
24
  - **Offline support switch** in the status header on the offline settings page. Off deletes
11
25
  what is stored, unregisters the worker, and hides the rest of the page.
12
26
  `config.caching_enabled_by_default` is the starting position of the switch (on). A device
data/README.md CHANGED
@@ -1,44 +1,45 @@
1
- # Coldwire
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
- 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.
8
+ [![Gem](https://img.shields.io/gem/v/coldwire-rails.svg)](https://rubygems.org/gems/coldwire-rails)
9
+ [![Gem](https://img.shields.io/gem/dt/coldwire-rails.svg)](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
- > **Status: early.** Extracted from a production app but young as a library. The API may
13
- > change before 1.0.
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
- - **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>
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-fallback.png" alt="The offline fallback: You're offline. This page isn't available offline. Reconnect and try again." width="280">
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>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
+ <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, 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).
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
1
+ 0.2.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
 
@@ -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` needs it if anyone signs in.
19
+ needs your attention on a first install; `cache_identity` already uses `current_user` or
20
+ `Current.user` when either is in scope.