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
data/docs/configuration.md
CHANGED
|
@@ -9,12 +9,25 @@ Coldwire.configure do |config|
|
|
|
9
9
|
config.auto_sync do |sync|
|
|
10
10
|
sync.enabled = false
|
|
11
11
|
sync.precache_urls = -> { [] }
|
|
12
|
-
sync.interval =
|
|
13
|
-
sync.max_age =
|
|
12
|
+
sync.interval = 1.day
|
|
13
|
+
sync.max_age = 30.days
|
|
14
14
|
sync.concurrency = 4
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
config.
|
|
17
|
+
config.garbage_collection do |gc|
|
|
18
|
+
gc.enabled = true
|
|
19
|
+
gc.max_age = 60.days
|
|
20
|
+
gc.max_size = 250.megabytes
|
|
21
|
+
gc.interval = 1.day
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config.cache_identity = -> {
|
|
25
|
+
if respond_to?(:current_user)
|
|
26
|
+
current_user&.id
|
|
27
|
+
elsif defined?(Current) && Current.respond_to?(:user)
|
|
28
|
+
Current.user&.id
|
|
29
|
+
end
|
|
30
|
+
}
|
|
18
31
|
config.register_if = -> { true }
|
|
19
32
|
config.caching_enabled_by_default = true
|
|
20
33
|
config.offline_import = "@hotwired/turbo-rails"
|
|
@@ -23,7 +36,7 @@ Coldwire.configure do |config|
|
|
|
23
36
|
config.never_cache = []
|
|
24
37
|
config.never_intercept = [ "/up" ]
|
|
25
38
|
|
|
26
|
-
config.
|
|
39
|
+
config.cacheable_hosts = []
|
|
27
40
|
config.cache_ranges = []
|
|
28
41
|
config.cache_archives = []
|
|
29
42
|
|
|
@@ -44,17 +57,21 @@ down with it.
|
|
|
44
57
|
|---|---|---|
|
|
45
58
|
| [`auto_sync.enabled`](#autosyncenabled) | `false` | Keep the precache manifest current on an interval |
|
|
46
59
|
| [`auto_sync.precache_urls`](#autosyncprecache_urls) | `-> { [] }` | The pages to fetch, evaluated against your URL helpers |
|
|
47
|
-
| [`auto_sync.interval`](#autosyncinterval) | `
|
|
48
|
-
| [`auto_sync.max_age`](#autosyncmax_age) | `
|
|
60
|
+
| [`auto_sync.interval`](#autosyncinterval) | `1.day` | How long to leave between syncs |
|
|
61
|
+
| [`auto_sync.max_age`](#autosyncmax_age) | `30.days` | Refetch a cached manifest page once it is older than this |
|
|
49
62
|
| [`auto_sync.concurrency`](#autosyncconcurrency) | `4` | Fetches in flight at once during a sync |
|
|
50
|
-
| [`
|
|
63
|
+
| [`garbage_collection.enabled`](#garbage_collectionenabled) | `true` | Sweep entries nothing has used in a long time |
|
|
64
|
+
| [`garbage_collection.max_age`](#garbage_collectionmax_age) | `60.days` | How long an entry may go untouched before it is collected |
|
|
65
|
+
| [`garbage_collection.max_size`](#garbage_collectionmax_size) | `250.megabytes` | How much the cache may hold; over it the least recently read go first |
|
|
66
|
+
| [`garbage_collection.interval`](#garbage_collectioninterval) | `1.day` | How long to leave between sweeps |
|
|
67
|
+
| [`cache_identity`](#cache_identity) | `current_user` / `Current.user` | Who the cache belongs to; changing it drops the cache |
|
|
51
68
|
| [`register_if`](#register_if) | `-> { true }` | Whether a page registers the worker at all |
|
|
52
69
|
| [`caching_enabled_by_default`](#caching_enabled_by_default) | `true` | Starting position of the Offline support switch. Not a master on/off |
|
|
53
70
|
| [`offline_import`](#offline_import) | `"@hotwired/turbo-rails"` | Importmap module the offline page loads to boot Turbo |
|
|
54
71
|
| [`cache_as_you_go`](#cache_as_you_go) | `["/*"]` | Pages stored as somebody browses. `/*` is everything |
|
|
55
72
|
| [`never_cache`](#never_cache) | `[]` | Never stored, by any route in. The one veto |
|
|
56
73
|
| [`never_intercept`](#never_intercept) | `["/up"]` | Paths the worker does not touch at all |
|
|
57
|
-
| [`
|
|
74
|
+
| [`cacheable_hosts`](#cacheable_hosts) | `[]` | Other hosts the worker may cache |
|
|
58
75
|
| [`cache_ranges`](#cache_ranges) | `[]` | URLs whose `Range` requests are cached piece by piece |
|
|
59
76
|
| [`cache_archives`](#cache_archives) | `[]` | Large files somebody can choose to download |
|
|
60
77
|
| [`ignore_query_params`](#ignore_query_params) | `true` | Treat `/map` and `/map?zoom=9` as one page |
|
|
@@ -74,8 +91,8 @@ fetched, an interval with no manifest has nothing to fetch.
|
|
|
74
91
|
config.auto_sync do |sync|
|
|
75
92
|
sync.enabled = true
|
|
76
93
|
sync.precache_urls = -> { Site.published.map { |site| site_path(site) } }
|
|
77
|
-
sync.interval =
|
|
78
|
-
sync.max_age =
|
|
94
|
+
sync.interval = 1.day
|
|
95
|
+
sync.max_age = 30.days
|
|
79
96
|
sync.concurrency = 4
|
|
80
97
|
end
|
|
81
98
|
```
|
|
@@ -105,7 +122,7 @@ The offline settings page has a per-device switch that turns automatic syncing o
|
|
|
105
122
|
`localStorage`. Switched off, no page holds a sync timer; **Sync now** still runs a pass.
|
|
106
123
|
|
|
107
124
|
<p align="center">
|
|
108
|
-
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, auto sync, and downloads" width="280">
|
|
125
|
+
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, the storage limit, auto sync, and downloads" width="280">
|
|
109
126
|
<img src="images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="280">
|
|
110
127
|
</p>
|
|
111
128
|
|
|
@@ -139,7 +156,7 @@ A stored page's stylesheets, scripts, and images are fetched with it, whatever t
|
|
|
139
156
|
|
|
140
157
|
### `auto_sync.interval`
|
|
141
158
|
|
|
142
|
-
**Default:** `
|
|
159
|
+
**Default:** `1.day`
|
|
143
160
|
|
|
144
161
|
How long to leave between syncs. An ActiveSupport duration works; the worker receives
|
|
145
162
|
seconds. Leave this long — a sync is a burst of fetches, not something to run on every
|
|
@@ -150,7 +167,7 @@ config change follows the new value rather than the one it was born with.
|
|
|
150
167
|
|
|
151
168
|
### `auto_sync.max_age`
|
|
152
169
|
|
|
153
|
-
**Default:** `
|
|
170
|
+
**Default:** `30.days`
|
|
154
171
|
|
|
155
172
|
Refetch a manifest page once its cached copy is older than this. `nil` fetches only what is
|
|
156
173
|
missing, so pages already cached are never noticed to have changed.
|
|
@@ -164,20 +181,141 @@ once would stall the app's own requests behind hundreds of connections.
|
|
|
164
181
|
|
|
165
182
|
---
|
|
166
183
|
|
|
184
|
+
## `garbage_collection`
|
|
185
|
+
|
|
186
|
+
A cache that fills as people browse fills forever. Collection takes back what nothing has
|
|
187
|
+
asked for in a long time.
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
config.garbage_collection do |gc|
|
|
191
|
+
gc.enabled = true
|
|
192
|
+
gc.max_age = 60.days
|
|
193
|
+
gc.max_size = 250.megabytes
|
|
194
|
+
gc.interval = 1.day
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Two limits, and a sweep applies both: `max_age` takes what has gone unused, then `max_size`
|
|
199
|
+
takes the least recently read of what is left until the cache fits. The second is the one that
|
|
200
|
+
binds on a device that browses far more than it revisits, where nothing is ever old enough to
|
|
201
|
+
collect and the cache grows until the browser evicts the lot.
|
|
202
|
+
|
|
203
|
+
**Only ever with a connection.** Deleting is the one cache operation with no way back:
|
|
204
|
+
whatever goes is gone until the network can be reached again. So a sweep pings
|
|
205
|
+
[`probe_path`](#probe_path) first and stands down if it cannot be reached, and stands down
|
|
206
|
+
under force offline. `navigator.onLine` is not consulted — a web view answers it wrongly often
|
|
207
|
+
enough to be worthless for a decision this expensive to get wrong.
|
|
208
|
+
|
|
209
|
+
**Untouched, not old.** Age is measured from when an entry was last *used*, not when it was
|
|
210
|
+
first fetched. Storing a page renews everything it names, so the stylesheet every page in your
|
|
211
|
+
app loads keeps a fresh date even though nothing ever refetches it. Without that, an asset
|
|
212
|
+
would carry the date of the very first page that pulled it in and be collected while the whole
|
|
213
|
+
app was still using it.
|
|
214
|
+
|
|
215
|
+
Renewal rewrites from the cache — it is never a network request — and only once an entry has
|
|
216
|
+
aged past a quarter of `max_age`. Under that it costs a lookup, so an ordinary navigation is
|
|
217
|
+
not rewriting every asset the page names.
|
|
218
|
+
|
|
219
|
+
Two things are never collected, whatever their age or how full the cache is — and neither is
|
|
220
|
+
counted against `max_size`, since the ceiling has to measure what a sweep can actually act on:
|
|
221
|
+
|
|
222
|
+
| | |
|
|
223
|
+
|---|---|
|
|
224
|
+
| **What the offline page needs** | browsing never touches it, and it is wanted precisely when there is no network |
|
|
225
|
+
| **Downloaded archives** | somebody chose to spend a data plan on those; disuse does not make them safe to throw away |
|
|
226
|
+
|
|
227
|
+
A sweep is paced by an open page the same way a sync is, for the same reason: nothing can wake
|
|
228
|
+
a worker in a WebKit web view. It is not recorded unless it actually ran, so a device that has
|
|
229
|
+
been offline for a week sweeps on its next page load with a connection.
|
|
230
|
+
|
|
231
|
+
### `garbage_collection.enabled`
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
config.garbage_collection { |gc| gc.enabled = false }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
On by default, unlike [`auto_sync`](#auto_sync). Syncing spends somebody's data plan, which is
|
|
238
|
+
theirs to opt into; a sweep spends nothing and the alternative is a cache that grows on their
|
|
239
|
+
phone until the browser evicts the whole thing. Off, nothing is collected and nothing is
|
|
240
|
+
renewed.
|
|
241
|
+
|
|
242
|
+
### `garbage_collection.max_age`
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
config.garbage_collection { |gc| gc.max_age = 60.days }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
How long an entry may go untouched before a sweep takes it. `nil` collects nothing, which is
|
|
249
|
+
the same as `enabled = false`.
|
|
250
|
+
|
|
251
|
+
Keep it comfortably longer than [`auto_sync.max_age`](#autosyncmax_age). A manifest page is
|
|
252
|
+
refetched once its copy is older than that, so as long as collection outlives refetching, a
|
|
253
|
+
sync brings a page back up to date well before a sweep would consider it. The defaults leave
|
|
254
|
+
60 days against 30.
|
|
255
|
+
|
|
256
|
+
### `garbage_collection.max_size`
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
config.garbage_collection { |gc| gc.max_size = 500.megabytes }
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
How much the cache may hold, in bytes. Past it a sweep deletes the least recently read entry,
|
|
263
|
+
then the next, until what is left fits — the same clock `max_age` works from, so an entry
|
|
264
|
+
renewed because a page still loads it is among the last to go rather than the first. `nil` is
|
|
265
|
+
no ceiling.
|
|
266
|
+
|
|
267
|
+
Measured over what a sweep is allowed to take, which is everything but the offline page's own
|
|
268
|
+
assets and downloaded archives. Counting a 300 MB download somebody deliberately kept would
|
|
269
|
+
empty the rest of the cache to make room for a file no sweep may touch.
|
|
270
|
+
|
|
271
|
+
The ceiling is applied when a sweep runs, so [`interval`](#garbage_collectioninterval) is also
|
|
272
|
+
how long the cache may sit over it. Lower the interval if a tighter bound matters more than
|
|
273
|
+
the work.
|
|
274
|
+
|
|
275
|
+
**People can change it.** The offline settings page offers a ladder of sizes — the configured
|
|
276
|
+
default always among them — and the choice is remembered in `localStorage` for that device,
|
|
277
|
+
the way Force offline and the Auto Sync switch are. It travels to the worker with each sweep,
|
|
278
|
+
since a worker cannot read `localStorage`. Changing it there sweeps immediately rather than
|
|
279
|
+
waiting out the interval.
|
|
280
|
+
|
|
281
|
+
### `garbage_collection.interval`
|
|
282
|
+
|
|
283
|
+
```ruby
|
|
284
|
+
config.garbage_collection { |gc| gc.interval = 12.hours }
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
How long to leave between sweeps. A sweep reads the cache index and deletes; there is nothing
|
|
288
|
+
to pace against a network, so this is about not doing pointless work on every page load rather
|
|
289
|
+
than about cost.
|
|
290
|
+
|
|
291
|
+
It is also how long the cache may exceed [`max_size`](#garbage_collectionmax_size), since that
|
|
292
|
+
is when the ceiling is applied. A sweep with a ceiling set costs a `match` per entry to measure
|
|
293
|
+
what is there, which is why it is worth pacing at all.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
167
297
|
## `cache_identity`
|
|
168
298
|
|
|
169
|
-
**Default:**
|
|
299
|
+
**Default:** `current_user&.id` or `Current.user&.id` when either is in scope
|
|
170
300
|
|
|
171
301
|
Who the cache belongs to, usually the signed-in user's id. Evaluated in the view, so
|
|
172
|
-
`current_user` is in scope
|
|
173
|
-
|
|
302
|
+
`current_user` is in scope — and `Current.user` if you keep the user there instead.
|
|
303
|
+
Recorded in `localStorage`; when it changes between page loads the cache is dropped —
|
|
304
|
+
which is what makes signing out, and switching accounts, safe.
|
|
174
305
|
|
|
175
306
|
```ruby
|
|
176
|
-
config.cache_identity = -> {
|
|
307
|
+
config.cache_identity = -> {
|
|
308
|
+
if respond_to?(:current_user)
|
|
309
|
+
current_user&.id
|
|
310
|
+
elsif defined?(Current) && Current.respond_to?(:user)
|
|
311
|
+
Current.user&.id
|
|
312
|
+
end
|
|
313
|
+
}
|
|
177
314
|
```
|
|
178
315
|
|
|
179
|
-
|
|
180
|
-
|
|
316
|
+
That is what the installer writes. If neither helper exists, the identity is empty and the
|
|
317
|
+
cache persists across sessions — fine for a single-user or fully public app. Override it
|
|
318
|
+
if your signed-in user lives somewhere else.
|
|
181
319
|
|
|
182
320
|
A few edges the setting already handles:
|
|
183
321
|
|
|
@@ -359,22 +497,28 @@ config.never_cache = [ %r{^/admin(/|$)}, %r{^/users/[^/]+/edit$} ]
|
|
|
359
497
|
|
|
360
498
|
---
|
|
361
499
|
|
|
362
|
-
## `
|
|
500
|
+
## `cacheable_hosts`
|
|
363
501
|
|
|
364
502
|
**Default:** `[]`
|
|
365
503
|
|
|
366
|
-
|
|
367
|
-
your app, or the response arrives opaque — status 0, no headers, no readable body — and
|
|
368
|
-
|
|
504
|
+
The hosts besides your own that the worker may cache. Each has to send CORS headers naming
|
|
505
|
+
your app, or the response arrives opaque — status 0, no headers, no readable body — and there
|
|
506
|
+
is nothing worth storing. Ranged sources must also expose `Content-Range`.
|
|
369
507
|
|
|
370
508
|
```ruby
|
|
371
|
-
config.
|
|
509
|
+
config.cacheable_hosts = [ "tiles.example.com", "localhost:3001" ]
|
|
372
510
|
```
|
|
373
511
|
|
|
374
|
-
|
|
375
|
-
|
|
512
|
+
Just the host. No scheme: a worker runs only on a secure page, and a secure page cannot fetch
|
|
513
|
+
`http`, so there was never a second scheme for one to tell apart. Writing one raises at boot
|
|
514
|
+
rather than being stripped — a scheme quietly accepted is a config that looks migrated and is
|
|
515
|
+
not, and the error names what to write instead.
|
|
516
|
+
|
|
517
|
+
A port only where it is not the default. A request is matched on its URL's `host`, against
|
|
518
|
+
exactly what you wrote: `localhost:3001` matches that port and no other, and a subdomain is a
|
|
519
|
+
different host.
|
|
376
520
|
|
|
377
|
-
|
|
521
|
+
Requests to anywhere else are passed straight through — not intercepted, not stored.
|
|
378
522
|
|
|
379
523
|
---
|
|
380
524
|
|
|
@@ -392,7 +536,7 @@ config.cache_ranges = [ "/tiles/*", %r{\.pmtiles$} ]
|
|
|
392
536
|
|
|
393
537
|
Patterns match the URL path, same as the other lists — not the full URL. A cross-origin
|
|
394
538
|
tile at `https://tiles.example.com/basemap.pmtiles` is allowed only when that origin is in
|
|
395
|
-
[`
|
|
539
|
+
[`cacheable_hosts`](#cacheable_hosts) *and* its path matches a rule here.
|
|
396
540
|
|
|
397
541
|
This pairs with [`cache_archives`](#cache_archives): `cache_ranges` caches the slices
|
|
398
542
|
actually read, so the places you have already opened work offline, and downloading the
|
|
@@ -423,7 +567,7 @@ Files arrive in 8 MB chunks, which is what makes a dropped connection cost secon
|
|
|
423
567
|
of the whole download. A `Range` request against a downloaded archive is answered by
|
|
424
568
|
slicing the chunks.
|
|
425
569
|
|
|
426
|
-
If the file lives on another
|
|
570
|
+
If the file lives on another host, list it in [`cacheable_hosts`](#cacheable_hosts).
|
|
427
571
|
|
|
428
572
|
---
|
|
429
573
|
|
|
@@ -478,14 +622,24 @@ for the first paint of a cold boot, before any JS runs, and a `<meta name="coldw
|
|
|
478
622
|
for Turbo visits, since Turbo merges the head but never copies `<html>` attributes.
|
|
479
623
|
`coldwire_service_worker_tag` mirrors the meta onto `<html>` on each `turbo:load`.
|
|
480
624
|
|
|
481
|
-
Any CSS can key off the attribute.
|
|
482
|
-
|
|
625
|
+
Any CSS can key off the attribute.
|
|
626
|
+
|
|
627
|
+
### Tailwind variants
|
|
628
|
+
|
|
629
|
+
With Tailwind v4, two custom variants give you `offline:` and `online:`:
|
|
483
630
|
|
|
484
631
|
```css
|
|
485
632
|
@custom-variant offline (html[data-coldwire-offline] &);
|
|
486
633
|
@custom-variant online (html:not([data-coldwire-offline]) &);
|
|
487
634
|
```
|
|
488
635
|
|
|
636
|
+
Then show or hide content from the markup:
|
|
637
|
+
|
|
638
|
+
```html
|
|
639
|
+
<p class="offline:hidden">You're online.</p>
|
|
640
|
+
<p class="online:hidden">You're looking at a cached page.</p>
|
|
641
|
+
```
|
|
642
|
+
|
|
489
643
|
From JavaScript, `window.Coldwire`:
|
|
490
644
|
|
|
491
645
|
```js
|
data/docs/how-it-works.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Six things break a naive offline cache in a Hotwire app. Four bite you in any browser; two
|
|
4
4
|
are Hotwire Native holding you to a stricter standard. Coldwire handles all six, which is
|
|
5
|
-
what lets one cache serve a plain Hotwire app, a PWA, and Hotwire Native.
|
|
5
|
+
what lets one cache serve a plain Hotwire web app, a PWA, and Hotwire Native.
|
|
6
6
|
|
|
7
7
|
When a visit has no cached copy and no network, people see this — a `200` that boots
|
|
8
8
|
Turbo — rather than a native error screen:
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 626 190" width="626" height="190" role="img" aria-label="Coldwire — Offline">
|
|
2
|
+
<g stroke-linejoin="round" stroke-linecap="round">
|
|
3
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
4
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
5
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
6
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
7
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
8
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
9
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
10
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
11
|
+
<g fill="#0B1220" stroke="#0B1220" stroke-width="8">
|
|
12
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
13
|
+
<circle cx="54" cy="92" r="26"/>
|
|
14
|
+
<circle cx="88" cy="66" r="36"/>
|
|
15
|
+
<circle cx="124" cy="78" r="28"/>
|
|
16
|
+
<circle cx="150" cy="94" r="22"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#38BDF8" stroke="none">
|
|
19
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
20
|
+
<circle cx="54" cy="92" r="26"/>
|
|
21
|
+
<circle cx="88" cy="66" r="36"/>
|
|
22
|
+
<circle cx="124" cy="78" r="28"/>
|
|
23
|
+
<circle cx="150" cy="94" r="22"/>
|
|
24
|
+
</g>
|
|
25
|
+
<g fill="none" stroke="#E6EDF3" stroke-width="9" stroke-linecap="round">
|
|
26
|
+
<path d="M 228.62 69.98 A 22 22 0 1 0 228.62 106.02"/>
|
|
27
|
+
<path d="M 254.00 88 A 22 22 0 1 0 298.00 88 A 22 22 0 1 0 254.00 88"/>
|
|
28
|
+
<path d="M 320 34 L 320 110"/>
|
|
29
|
+
<path d="M 346.00 88 A 22 22 0 1 0 390.00 88 A 22 22 0 1 0 346.00 88"/>
|
|
30
|
+
<path d="M 390 34 L 390 110"/>
|
|
31
|
+
<path d="M 410 66 L 421 110 L 432 74 L 443 110 L 454 66"/>
|
|
32
|
+
<path d="M 476 66 L 476 110"/>
|
|
33
|
+
<path d="M 476 42 L 476 42"/>
|
|
34
|
+
<path d="M 506 66 L 506 110"/>
|
|
35
|
+
<path d="M 506 82 A 20 20 0 0 1 530 66"/>
|
|
36
|
+
<path d="M 590.00 88.00 L 546.00 88"/>
|
|
37
|
+
<path d="M 590.00 88.00 A 22 22 0 1 0 579.00 107.05"/>
|
|
38
|
+
</g>
|
|
39
|
+
<g fill="none" stroke="#94A3B8" stroke-width="9" stroke-linecap="round" transform="translate(445.12 107.40) scale(0.46)">
|
|
40
|
+
<path d="M 0.00 88 A 22 22 0 1 0 44.00 88 A 22 22 0 1 0 0.00 88"/>
|
|
41
|
+
<path d="M 75 110 L 75 48"/>
|
|
42
|
+
<path d="M 75 48 A 17 17 0 0 1 92 34"/>
|
|
43
|
+
<path d="M 62 66 L 91 66"/>
|
|
44
|
+
<path d="M 121 110 L 121 48"/>
|
|
45
|
+
<path d="M 121 48 A 17 17 0 0 1 138 34"/>
|
|
46
|
+
<path d="M 108 66 L 137 66"/>
|
|
47
|
+
<path d="M 158 34 L 158 110"/>
|
|
48
|
+
<path d="M 188 66 L 188 110"/>
|
|
49
|
+
<path d="M 188 42 L 188 42"/>
|
|
50
|
+
<path d="M 218 66 L 218 110"/>
|
|
51
|
+
<path d="M 218 86 A 20 20 0 0 1 258 86"/>
|
|
52
|
+
<path d="M 258 86 L 258 110"/>
|
|
53
|
+
<path d="M 322.00 88.00 L 278.00 88"/>
|
|
54
|
+
<path d="M 322.00 88.00 A 22 22 0 1 0 311.00 107.05"/>
|
|
55
|
+
</g>
|
|
56
|
+
</g>
|
|
57
|
+
</svg>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 626 190" width="626" height="190" role="img" aria-label="Coldwire — Offline">
|
|
2
|
+
<g stroke-linejoin="round" stroke-linecap="round">
|
|
3
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
4
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
5
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
6
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
7
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
8
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
9
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
10
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
11
|
+
<g fill="#0F172A" stroke="#0F172A" stroke-width="8">
|
|
12
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
13
|
+
<circle cx="54" cy="92" r="26"/>
|
|
14
|
+
<circle cx="88" cy="66" r="36"/>
|
|
15
|
+
<circle cx="124" cy="78" r="28"/>
|
|
16
|
+
<circle cx="150" cy="94" r="22"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#0EA5E9" stroke="none">
|
|
19
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
20
|
+
<circle cx="54" cy="92" r="26"/>
|
|
21
|
+
<circle cx="88" cy="66" r="36"/>
|
|
22
|
+
<circle cx="124" cy="78" r="28"/>
|
|
23
|
+
<circle cx="150" cy="94" r="22"/>
|
|
24
|
+
</g>
|
|
25
|
+
<g fill="none" stroke="#0F172A" stroke-width="9" stroke-linecap="round">
|
|
26
|
+
<path d="M 228.62 69.98 A 22 22 0 1 0 228.62 106.02"/>
|
|
27
|
+
<path d="M 254.00 88 A 22 22 0 1 0 298.00 88 A 22 22 0 1 0 254.00 88"/>
|
|
28
|
+
<path d="M 320 34 L 320 110"/>
|
|
29
|
+
<path d="M 346.00 88 A 22 22 0 1 0 390.00 88 A 22 22 0 1 0 346.00 88"/>
|
|
30
|
+
<path d="M 390 34 L 390 110"/>
|
|
31
|
+
<path d="M 410 66 L 421 110 L 432 74 L 443 110 L 454 66"/>
|
|
32
|
+
<path d="M 476 66 L 476 110"/>
|
|
33
|
+
<path d="M 476 42 L 476 42"/>
|
|
34
|
+
<path d="M 506 66 L 506 110"/>
|
|
35
|
+
<path d="M 506 82 A 20 20 0 0 1 530 66"/>
|
|
36
|
+
<path d="M 590.00 88.00 L 546.00 88"/>
|
|
37
|
+
<path d="M 590.00 88.00 A 22 22 0 1 0 579.00 107.05"/>
|
|
38
|
+
</g>
|
|
39
|
+
<g fill="none" stroke="#64748B" stroke-width="9" stroke-linecap="round" transform="translate(445.12 107.40) scale(0.46)">
|
|
40
|
+
<path d="M 0.00 88 A 22 22 0 1 0 44.00 88 A 22 22 0 1 0 0.00 88"/>
|
|
41
|
+
<path d="M 75 110 L 75 48"/>
|
|
42
|
+
<path d="M 75 48 A 17 17 0 0 1 92 34"/>
|
|
43
|
+
<path d="M 62 66 L 91 66"/>
|
|
44
|
+
<path d="M 121 110 L 121 48"/>
|
|
45
|
+
<path d="M 121 48 A 17 17 0 0 1 138 34"/>
|
|
46
|
+
<path d="M 108 66 L 137 66"/>
|
|
47
|
+
<path d="M 158 34 L 158 110"/>
|
|
48
|
+
<path d="M 188 66 L 188 110"/>
|
|
49
|
+
<path d="M 188 42 L 188 42"/>
|
|
50
|
+
<path d="M 218 66 L 218 110"/>
|
|
51
|
+
<path d="M 218 86 A 20 20 0 0 1 258 86"/>
|
|
52
|
+
<path d="M 258 86 L 258 110"/>
|
|
53
|
+
<path d="M 322.00 88.00 L 278.00 88"/>
|
|
54
|
+
<path d="M 322.00 88.00 A 22 22 0 1 0 311.00 107.05"/>
|
|
55
|
+
</g>
|
|
56
|
+
</g>
|
|
57
|
+
</svg>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="22 22 158 142" width="158" height="142" role="img" aria-label="Coldwire">
|
|
2
|
+
<g stroke-linejoin="round" stroke-linecap="round">
|
|
3
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
4
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
5
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
6
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#0B1220" stroke="#0B1220" stroke-width="3.6"/>
|
|
7
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
8
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
9
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
10
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#F0F9FF" stroke="none"/>
|
|
11
|
+
<g fill="#0B1220" stroke="#0B1220" stroke-width="8">
|
|
12
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
13
|
+
<circle cx="54" cy="92" r="26"/>
|
|
14
|
+
<circle cx="88" cy="66" r="36"/>
|
|
15
|
+
<circle cx="124" cy="78" r="28"/>
|
|
16
|
+
<circle cx="150" cy="94" r="22"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#38BDF8" stroke="none">
|
|
19
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
20
|
+
<circle cx="54" cy="92" r="26"/>
|
|
21
|
+
<circle cx="88" cy="66" r="36"/>
|
|
22
|
+
<circle cx="124" cy="78" r="28"/>
|
|
23
|
+
<circle cx="150" cy="94" r="22"/>
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
</svg>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="22 22 158 142" width="158" height="142" role="img" aria-label="Coldwire">
|
|
2
|
+
<g stroke-linejoin="round" stroke-linecap="round">
|
|
3
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
4
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
5
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
6
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#0F172A" stroke="#0F172A" stroke-width="3.6"/>
|
|
7
|
+
<path d="M 57.24 107.00 C 57.44 116.36 62.24 125.20 64.00 133.00 C 65.76 125.20 70.56 116.36 70.76 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
8
|
+
<path d="M 77.60 107.00 C 77.91 121.40 85.30 135.00 88.00 147.00 C 90.70 135.00 98.09 121.40 98.40 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
9
|
+
<path d="M 104.20 107.00 C 104.43 117.80 109.97 128.00 112.00 137.00 C 114.03 128.00 119.57 117.80 119.80 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
10
|
+
<path d="M 129.32 107.00 C 129.46 113.48 132.78 119.60 134.00 125.00 C 135.22 119.60 138.54 113.48 138.68 107.00 Z" fill="#E0F2FE" stroke="none"/>
|
|
11
|
+
<g fill="#0F172A" stroke="#0F172A" stroke-width="8">
|
|
12
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
13
|
+
<circle cx="54" cy="92" r="26"/>
|
|
14
|
+
<circle cx="88" cy="66" r="36"/>
|
|
15
|
+
<circle cx="124" cy="78" r="28"/>
|
|
16
|
+
<circle cx="150" cy="94" r="22"/>
|
|
17
|
+
</g>
|
|
18
|
+
<g fill="#0EA5E9" stroke="none">
|
|
19
|
+
<rect x="54" y="58.80" width="96" height="57.20" rx="28.60" ry="28.60"/>
|
|
20
|
+
<circle cx="54" cy="92" r="26"/>
|
|
21
|
+
<circle cx="88" cy="66" r="36"/>
|
|
22
|
+
<circle cx="124" cy="78" r="28"/>
|
|
23
|
+
<circle cx="150" cy="94" r="22"/>
|
|
24
|
+
</g>
|
|
25
|
+
</g>
|
|
26
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
data/docs/setup.md
CHANGED
|
@@ -6,7 +6,7 @@ provides `Turbo`. Add the gem, then let the installer wire the rest.
|
|
|
6
6
|
## Requirements
|
|
7
7
|
|
|
8
8
|
- Rails 7.1+
|
|
9
|
-
- Turbo — a plain Hotwire app, a PWA, or Hotwire Native
|
|
9
|
+
- Turbo — a plain Hotwire web app, a PWA, or Hotwire Native
|
|
10
10
|
- Service workers, and HTTPS (or localhost). They are same-origin, so the engine has to be
|
|
11
11
|
mounted on the app's own domain
|
|
12
12
|
- Hotwire Native is optional. Nothing here requires it
|
|
@@ -102,9 +102,9 @@ Every option has a working default. The full list, and what each one does, is in
|
|
|
102
102
|
|
|
103
103
|
## What to set first
|
|
104
104
|
|
|
105
|
-
**`cache_identity`**, if
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
**`cache_identity`**, if the signed-in user is not `current_user` or `Current.user`. Cached
|
|
106
|
+
pages hold whatever the session that fetched them could see. The installer already uses
|
|
107
|
+
those two when they are in scope; override it if yours lives somewhere else.
|
|
108
108
|
|
|
109
109
|
```ruby
|
|
110
110
|
config.cache_identity = -> { current_user&.id }
|
|
@@ -136,7 +136,8 @@ Mounted at the engine root — `/offline` with the mount above. It inherits your
|
|
|
136
136
|
`ApplicationController`, so it picks up your layout, authentication, and helpers.
|
|
137
137
|
|
|
138
138
|
This is the page people use to turn offline support on or off, see connection status, download
|
|
139
|
-
archives, turn auto-sync off for this device,
|
|
139
|
+
archives, turn auto-sync off for this device, set how much storage the cache may use, force
|
|
140
|
+
offline, and manage what is cached.
|
|
140
141
|
Turning offline support off asks first, then deletes what is stored and hides the rest of the
|
|
141
142
|
page. It sets `content_for :title` to `"Offline settings"` — yield that in your layout's
|
|
142
143
|
`<title>` (and any native title bar that reads it) rather than expecting an on-page heading.
|
|
@@ -144,13 +145,17 @@ Put it behind whatever authentication you use by wrapping the route, or override
|
|
|
144
145
|
`app/views/coldwire/caches/show.html.erb`.
|
|
145
146
|
|
|
146
147
|
<p align="center">
|
|
147
|
-
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, auto sync, and downloads" width="280">
|
|
148
|
+
<img src="images/offline-settings.png" alt="Offline settings: status, force offline, the storage limit, auto sync, and downloads" width="280">
|
|
148
149
|
<img src="images/offline-settings-cached.png" alt="Offline settings: every cached entry, with search, sort, and delete" width="280">
|
|
149
150
|
</p>
|
|
150
151
|
|
|
151
152
|
To reach it offline, list it in `cache_as_you_go` like any other page. **Sync now** talks to
|
|
152
153
|
the manifest, which is never intercepted, so that button fails while offline; **Inspect cache**,
|
|
153
|
-
**Clear cache**, and **Force offline** are client-side and keep working.
|
|
154
|
+
**Clear cache**, and **Force offline** are client-side and keep working. **Keep at most** —
|
|
155
|
+
the storage ceiling, from
|
|
156
|
+
[`garbage_collection.max_size`](configuration.md#garbage_collectionmax_size) — is remembered
|
|
157
|
+
straight away, but the sweep it triggers needs a connection like any other, so a lowered
|
|
158
|
+
ceiling applies once there is one. The URL list lives
|
|
154
159
|
under Inspect cache, closed until you open it.
|
|
155
160
|
|
|
156
161
|
## Hotwire Native on iOS
|
|
@@ -171,7 +176,7 @@ app-bound mode and takes service workers with it.
|
|
|
171
176
|
## Optional next steps
|
|
172
177
|
|
|
173
178
|
- Restrict what browsing stores with [`cache_as_you_go`](configuration.md#cache_as_you_go)
|
|
174
|
-
- Nominate other
|
|
179
|
+
- Nominate other hosts or `Range` URLs with [`cacheable_hosts`](configuration.md#cacheable_hosts)
|
|
175
180
|
and [`cache_ranges`](configuration.md#cache_ranges)
|
|
176
181
|
- Offer large files for download with [`cache_archives`](configuration.md#cache_archives)
|
|
177
182
|
- Override the offline fallback by creating
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
// Turbo copies head scripts it does not recognise, and a per-request CSP nonce makes this
|
|
3
|
+
// one look new on every visit — without the guard each visit wires another listener.
|
|
4
|
+
if (window.__coldwireCollect) return
|
|
5
|
+
window.__coldwireCollect = true
|
|
6
|
+
|
|
7
|
+
var store = window.coldwireStore
|
|
8
|
+
var keys = store.keys
|
|
9
|
+
var interval = COLDWIRE.collectInterval
|
|
10
|
+
|
|
11
|
+
// Nothing like the sync scheduler here on purpose. A sweep is quick, takes no network worth
|
|
12
|
+
// pacing, and two of them at once is harmless — so there is no claim to hold, no countdown
|
|
13
|
+
// to draw, and no progress to report. A stamp and a check are the whole clock.
|
|
14
|
+
function due() {
|
|
15
|
+
var last = store.number(keys.collectedAt)
|
|
16
|
+
|
|
17
|
+
return !last || Date.now() - last >= interval
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function run() {
|
|
21
|
+
if (!due()) return
|
|
22
|
+
if (store.on(keys.forced)) return
|
|
23
|
+
if (!store.cachingOn()) return
|
|
24
|
+
if (!("serviceWorker" in navigator)) return
|
|
25
|
+
|
|
26
|
+
navigator.serviceWorker.ready.then(function (registration) {
|
|
27
|
+
var worker = registration.active || navigator.serviceWorker.controller
|
|
28
|
+
if (!worker) return
|
|
29
|
+
|
|
30
|
+
var channel = new MessageChannel()
|
|
31
|
+
channel.port1.onmessage = function (event) {
|
|
32
|
+
var result = event.data
|
|
33
|
+
// No connection, or force offline: nothing was swept, so leave the clock alone and
|
|
34
|
+
// let the next page find it still due.
|
|
35
|
+
if (!result || result.offline) return
|
|
36
|
+
|
|
37
|
+
store.set(keys.collectedAt, Date.now())
|
|
38
|
+
}
|
|
39
|
+
// The ceiling travels with the request: it lives in localStorage, which a worker
|
|
40
|
+
// cannot read, and it is a choice this device made rather than one the app baked in.
|
|
41
|
+
worker.postMessage({ type: "collect", maxSize: store.maxSize() }, [ channel.port2 ])
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
document.addEventListener("turbo:load", run)
|
|
46
|
+
window.addEventListener("online", run)
|
|
47
|
+
|
|
48
|
+
run()
|
|
49
|
+
})();
|