Files
editor/src/libs/stylestore.ts
T
Bart Louwers 599240033a Handle QuotaExceededError in StyleStore (#1253)
## Launch Checklist

<!-- Thanks for the PR! Feel free to add or remove items from the
checklist. -->

When localStorage is full you start getting a "QuotaExceededError".
RIght now Maputnik does not handle this situation gracefully, it just
fails loading the style with a non-descriptive error message.

This PR purges localStorage and tries again when this particular error
happens.

It still does not show a descriptive error message. If you try to load a
style that is larger than what localStorage can handle, it will still
fail with a non-descriptive error message.

Increased the size of `example-style.json` so that it causes a
QuotaExceededError when running the regression test (try it before and
after this PR).


 - [x] Briefly describe the changes in this PR.
 - [x] Link to related issues. N/A
- [x] Include before/after visuals or gifs if this PR includes visual
changes. N/A
 - [x] Write tests for all new functionality.
 - [x] Add an entry to `CHANGELOG.md` under the `## main` section.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-07-04 10:27:00 +02:00

119 lines
3.2 KiB
TypeScript

import style from './style'
import {loadStyleUrl} from './urlopen'
import publicSources from '../config/styles.json'
import type {StyleSpecification} from 'maplibre-gl'
const storagePrefix = "maputnik"
const stylePrefix = 'style'
const storageKeys = {
latest: [storagePrefix, 'latest_style'].join(':'),
accessToken: [storagePrefix, 'access_token'].join(':')
}
const defaultStyleUrl = publicSources[0].url
// Fetch a default style via URL and return it or a fallback style via callback
export function loadDefaultStyle(cb: (...args: any[]) => void) {
loadStyleUrl(defaultStyleUrl, cb)
}
// Return style ids and dates of all styles stored in local storage
function loadStoredStyles() {
const styles = []
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i)
if(isStyleKey(key!)) {
styles.push(fromKey(key!))
}
}
return styles
}
function isStyleKey(key: string) {
const parts = key.split(":")
return parts.length === 3 && parts[0] === storagePrefix && parts[1] === stylePrefix
}
// Load style id from key
function fromKey(key: string) {
if(!isStyleKey(key)) {
throw "Key is not a valid style key"
}
const parts = key.split(":")
const styleId = parts[2]
return styleId
}
// Calculate key that identifies the style with a version
function styleKey(styleId: string) {
return [storagePrefix, stylePrefix, styleId].join(":")
}
// Manages many possible styles that are stored in the local storage
export class StyleStore {
/**
* List of style ids
*/
mapStyles: string[];
// Tile store will load all items from local storage and
// assume they do not change will working on it
constructor() {
this.mapStyles = loadStoredStyles();
}
init(cb: (...args: any[]) => void) {
cb(null)
}
// Delete entire style history
purge() {
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i) as string;
if(key.startsWith(storagePrefix)) {
window.localStorage.removeItem(key)
}
}
}
// Find the last edited style
latestStyle(cb: (...args: any[]) => void) {
if(this.mapStyles.length === 0) return loadDefaultStyle(cb)
const styleId = window.localStorage.getItem(storageKeys.latest) as string;
const styleItem = window.localStorage.getItem(styleKey(styleId))
if(styleItem) return cb(JSON.parse(styleItem))
loadDefaultStyle(cb)
}
// Save current style replacing previous version
save(mapStyle: StyleSpecification & { id: string }) {
mapStyle = style.ensureStyleValidity(mapStyle)
const key = styleKey(mapStyle.id)
const saveFn = () => {
window.localStorage.setItem(key, JSON.stringify(mapStyle))
window.localStorage.setItem(storageKeys.latest, mapStyle.id)
}
try {
saveFn()
} catch (e) {
// Handle quota exceeded error
if (e instanceof DOMException && (
e.code === 22 || // Firefox
e.code === 1014 || // Firefox
e.name === 'QuotaExceededError' ||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED'
)) {
this.purge()
saveFn() // Retry after clearing
} else {
throw e
}
}
return mapStyle
}
}