ReviewOS

also looking at this

stacks/ts-maps

fix: resolve #6259 Offline: document and support caching the app sh...

#6270
Draft chenzeyan54-commits wants to merge fix/issue-6259 into main
1 file +1 -130

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/concepts/offline.mdmodified+1-130
Changes to docs/concepts/offline.md
@@ -4,133 +4,4 @@ Download an area once and the map, search and directions keep working there with
44
55## Offline maps
66
7The quickest route is the control. It adds a download button to the map:
8
9```ts
10import { control } from 'ts-maps'
11
12control.offlineMaps().addTo(map)
13```
14
15The button opens **Offline Maps**, a list of what has been downloaded, each with its size, date, and a progress bar while it downloads. Each row has Pause/Resume, Update and Delete; Delete asks once more before it deletes. Tapping a name flies to the area and outlines it.
16
17**Download New Map** dims the map around a rounded frame. Drag its corners, or move the map beneath it, to choose the area. The card below names the area after the most important place labelled inside it (pass `geocoder` to name it by reverse geocoding instead), and shows an estimated size before anything is fetched. The estimate comes from sampling a few of the area's own tiles.
18
19When the browser goes offline, a pill at the top says so, and says whether a downloaded map covers the view. The **Only Use Offline Maps** switch keeps the map off the network entirely.
20
21| Option | Default | |
22| --- | --- | --- |
23| `position` | `'topright'` | |
24| `maps` | `offlineMaps()` | The manager to show. |
25| `geocoder` | — | Names a new area by reverse geocoding its centre. |
26| `resources` | — | Other URLs to keep with every download, such as a TileJSON your page fetches itself. |
27| `showStatus` | `true` | The offline pill. |
28
29Every framework binding has it as `<OfflineMaps>`, with `open` and
30`onlyOffline` followed as props — see
31[framework bindings](../guide/framework-bindings.md#offline-maps).
32
33### From code
34
35`map.offline` is the manager behind the control, and `offlineMaps()` returns the same object:
36
37```ts
38// What an area would cost, before downloading it.
39const { tiles, bytes, tooLarge } = await map.offline.estimate({ bounds: [-122.45, 37.76, -122.39, 37.81] })
40
41// Download it. Resolves when it is complete, paused or cancelled.
42map.offline.on('progress', ({ region }) => console.log(region.downloaded, '/', region.tiles))
43const region = await map.offline.download({ bounds: [-122.45, 37.76, -122.39, 37.81], name: 'San Francisco' })
44
45await map.offline.list() // every downloaded map, newest first
46await map.offline.pause(region.id)
47await map.offline.resume(region.id) // carries on from where it stopped
48await map.offline.update(region.id) // fetches every tile again
49await map.offline.rename(region.id, 'Home')
50await map.offline.delete(region.id)
51await map.offline.usage() // { bytes, entries } on this device
52```
53
54- **Which tiles are downloaded.** An area is downloaded for every tile layer on the map. Each layer is asked for its URLs exactly as it asks while drawing: subdomain, retina suffix and 512px zoom shift included. What is stored is therefore exactly what the map later requests.
55- **Zoom range.** A vector source is kept to its top zoom by default; past that, its tiles are drawn sharply by overzooming. Image layers stop at zoom 16 unless `maxZoom` says otherwise.
56- **Style files.** The style's sprite sheets, the common glyph ranges, and the style document (when it was loaded from a URL) are kept too.
57- **Without a map.** Download from URL templates by passing `sources: [{ url, type, tileSize, maxZoom }]`.
58- **Size limit.** An area over `maxTiles` (150,000 by default) is refused; `estimate()` reports it as `tooLarge`.
59
60Downloads are kept in IndexedDB and survive a reload. A download interrupted by a reload comes back paused. Tiles that two areas share are stored once, and deleting one area keeps whatever the other still needs. Events on the manager: `change`, `progress`, `complete`, `error` and `delete`.
61
62### How the map uses them
63
64Every tile, style, sprite, glyph and terrain request checks the downloaded maps first. A hit costs no network and no data. A miss goes to the network as usual, or fails straight away when `onlyOffline` is set:
65
66```ts
67map.offline.onlyOffline = true // Apple's "Only Use Offline Maps"
68map.offline.enabled = false // ignore downloads altogether
69```
70
71A page that has never downloaded anything never opens a database: where the browser can say none exists, none is created.
72
73### Search and directions offline
74
75Downloading reads each vector tile at its top zoom for:
76
77- the places, streets and points of interest named in it;
78- its road network, with one-way streets, ramps, bridges and tunnels.
79
80Both are available as ordinary providers:
81
82```ts
83import { withOfflineFallback } from 'ts-maps'
84import { NominatimGeocoder, OSRMDirections } from 'ts-maps/services'
85
86const geocoder = withOfflineFallback(new NominatimGeocoder(), map.offline.geocoder())
87const directions = withOfflineFallback(new OSRMDirections(), map.offline.directions())
88```
89
90`withOfflineFallback` uses the online provider when it can. It switches to the offline one when the browser reports no connection, or when the online request fails for any reason other than being cancelled.
91
92The offline router runs A* over the downloaded roads for driving, walking or cycling, with alternatives. Its steps use the same maneuver codes as the online providers, so `turnByTurn`, the banner and the voice work unchanged: "Turn left onto Broadway", "Turn right onto Van Ness Avenue".
93
94Tiles are simplified when they are made, which drops junction vertices along straight streets. The graph finds those junctions again: at every crossing, and wherever a road ends on another. Roads at different levels are never joined, so a bridge does not connect to the street below it.
95
96Search, reverse geocoding and routing read the OpenMapTiles schema used by the built-in styles.
97
98## Tile cache
99
100`TileCache` is a lower-level, promise-based key/value store of tile bytes keyed by URL, with optional TTL and LRU limits. It is in memory unless you give it a backend. `cachedFetch` reads through it: a hit returns immediately, and a miss goes to the network and is stored. If the network fails, it falls back to whatever the cache holds.
101
102```ts
103import { cachedFetch, TileCache } from 'ts-maps'
104
105const cache = new TileCache({ maxBytes: 200 * 1024 * 1024 })
106const { data, fromCache } = await cachedFetch('https://tile.openstreetmap.org/10/301/384.png', { cache })
107```
108
109Tile layers accept `offlineCache: cache` (or `true` for the shared `getDefaultCache()`) to read through one. Downloaded offline maps are always consulted first.
110
111### Lifecycle
112
113`close()` releases the backend. It is idempotent, and the cache stays usable: the next `get` / `put` reopens it. For the shared singleton, use `resetDefaultCache()`.
114
115### Pluggable backends
116
117`new TileCache({ backend })` accepts any object implementing `get` / `put` / `delete` / `clear` / `all` / optional `close`. This is how `@craft-native/ts-maps` stores tiles in the Craft sandbox filesystem. Offline maps have their own store contract, `OfflineStore`, with an IndexedDB and an in-memory implementation; pass one as `new OfflineMaps({ store })`.
118
119### One-shot prefetch
120
121`saveOfflineRegion({ bounds, zoomRange, tileUrl, cache })` fills a `TileCache` with every tile of one URL template in a bounding box. It predates offline maps: it keeps no record of the area, and the area cannot be paused, resumed, updated or deleted. Prefer `map.offline.download()`.
122
123## Worker pool
124
125`WorkerPool` round-robins jobs across a fixed number of Web Workers. Vector tile decode, heatmap rasterisation, and hillshade computation all route through it by default.
126
127```ts
128import { WorkerPool } from 'ts-maps'
129
130const pool = new WorkerPool({ size: navigator.hardwareConcurrency ?? 4 })
131const decoded = await pool.run('decodeMvt', { bytes, extent: 4096 })
132```
133
134## Try it
135
136The playground's **14. Offline maps** page has the whole flow. Pick an area, download it, then either switch on "Only Use Offline Maps" or take the browser offline; the downloaded area keeps drawing. The older [offline example](../examples/11-offline.md) shows the lower-level `TileCache` and region prefetcher on their own.
7The quickest route is the control. It adds a download button to the map: