mirror of
https://github.com/maputnik/editor.git
synced 2026-07-31 10:17:26 +00:00
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>
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
- Upgrade to MapLibre LG JS v5
|
- Upgrade to MapLibre LG JS v5
|
||||||
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
- Upgrade Vite 6 and Cypress 14 ([#970](https://github.com/maplibre/maputnik/pull/970))
|
||||||
- Upgrade OpenLayers from v6 to v10
|
- Upgrade OpenLayers from v6 to v10
|
||||||
|
- When loading a style into localStorage that causes a QuotaExceededError, purge localStorage and retry
|
||||||
- _...Add new stuff here..._
|
- _...Add new stuff here..._
|
||||||
|
|
||||||
### 🐞 Bug fixes
|
### 🐞 Bug fixes
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ const baseUrl = "http://localhost:8888/";
|
|||||||
|
|
||||||
const styleFromWindow = (win: Window) => {
|
const styleFromWindow = (win: Window) => {
|
||||||
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
const styleId = win.localStorage.getItem("maputnik:latest_style");
|
||||||
const styleItem = win.localStorage.getItem(`maputnik:style:${styleId}`);
|
const styleItemKey = `maputnik:style:${styleId}`;
|
||||||
const obj = JSON.parse(styleItem || "");
|
const styleItem = win.localStorage.getItem(styleItemKey);
|
||||||
|
if (!styleItem) throw new Error("Could not get styleItem from localStorage");
|
||||||
|
const obj = JSON.parse(styleItem);
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -275,4 +275,44 @@ describe("modals", () => {
|
|||||||
describe("sources", () => {
|
describe("sources", () => {
|
||||||
it("toggle");
|
it("toggle");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Handle localStorage QuotaExceededError", () => {
|
||||||
|
it("handles quota exceeded error when opening style from URL", () => {
|
||||||
|
// Clear localStorage to start fresh
|
||||||
|
cy.clearLocalStorage();
|
||||||
|
|
||||||
|
// fill localStorage until we get a QuotaExceededError
|
||||||
|
cy.window().then(win => {
|
||||||
|
let chunkSize = 1000;
|
||||||
|
const chunk = new Array(chunkSize).join("x");
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
// Keep adding until we hit the quota
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const key = `maputnik:fill-${index++}`;
|
||||||
|
win.localStorage.setItem(key, chunk);
|
||||||
|
} catch (e: any) {
|
||||||
|
// Verify it's a quota error
|
||||||
|
if (e.name === 'QuotaExceededError') {
|
||||||
|
if (chunkSize <= 1) return;
|
||||||
|
else {
|
||||||
|
chunkSize /= 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e; // Unexpected error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Open the style via URL input
|
||||||
|
when.click("nav:open");
|
||||||
|
when.setValue("modal:open.url.input", get.exampleFileUrl());
|
||||||
|
when.click("modal:open.url.button");
|
||||||
|
|
||||||
|
then(get.responseBody("example-style.json")).shouldEqualToStoredStyle();
|
||||||
|
then(get.styleFromLocalStorage()).shouldExist();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,114 @@
|
|||||||
{
|
{
|
||||||
"id": "test-style",
|
"id": "test-style",
|
||||||
"version": 8,
|
"version": 8,
|
||||||
"name": "Test Style",
|
"name": "Test Style",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"maputnik:renderer": "mlgljs"
|
"maputnik:renderer": "mlgljs",
|
||||||
},
|
"data": [
|
||||||
"sources": {},
|
0,
|
||||||
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
1,
|
||||||
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
2,
|
||||||
"layers": []
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9,
|
||||||
|
10,
|
||||||
|
11,
|
||||||
|
12,
|
||||||
|
13,
|
||||||
|
14,
|
||||||
|
15,
|
||||||
|
16,
|
||||||
|
17,
|
||||||
|
18,
|
||||||
|
19,
|
||||||
|
20,
|
||||||
|
21,
|
||||||
|
22,
|
||||||
|
23,
|
||||||
|
24,
|
||||||
|
25,
|
||||||
|
26,
|
||||||
|
27,
|
||||||
|
28,
|
||||||
|
29,
|
||||||
|
30,
|
||||||
|
31,
|
||||||
|
32,
|
||||||
|
33,
|
||||||
|
34,
|
||||||
|
35,
|
||||||
|
36,
|
||||||
|
37,
|
||||||
|
38,
|
||||||
|
39,
|
||||||
|
40,
|
||||||
|
41,
|
||||||
|
42,
|
||||||
|
43,
|
||||||
|
44,
|
||||||
|
45,
|
||||||
|
46,
|
||||||
|
47,
|
||||||
|
48,
|
||||||
|
49,
|
||||||
|
50,
|
||||||
|
51,
|
||||||
|
52,
|
||||||
|
53,
|
||||||
|
54,
|
||||||
|
55,
|
||||||
|
56,
|
||||||
|
57,
|
||||||
|
58,
|
||||||
|
59,
|
||||||
|
60,
|
||||||
|
61,
|
||||||
|
62,
|
||||||
|
63,
|
||||||
|
64,
|
||||||
|
65,
|
||||||
|
66,
|
||||||
|
67,
|
||||||
|
68,
|
||||||
|
69,
|
||||||
|
70,
|
||||||
|
71,
|
||||||
|
72,
|
||||||
|
73,
|
||||||
|
74,
|
||||||
|
75,
|
||||||
|
76,
|
||||||
|
77,
|
||||||
|
78,
|
||||||
|
79,
|
||||||
|
80,
|
||||||
|
81,
|
||||||
|
82,
|
||||||
|
83,
|
||||||
|
84,
|
||||||
|
85,
|
||||||
|
86,
|
||||||
|
87,
|
||||||
|
88,
|
||||||
|
89,
|
||||||
|
90,
|
||||||
|
91,
|
||||||
|
92,
|
||||||
|
93,
|
||||||
|
94,
|
||||||
|
95,
|
||||||
|
96,
|
||||||
|
97,
|
||||||
|
98,
|
||||||
|
99
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sources": {},
|
||||||
|
"glyphs": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"sprites": "https://example.local/fonts/{fontstack}/{range}.pbf",
|
||||||
|
"layers": []
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -91,8 +91,28 @@ export class StyleStore {
|
|||||||
save(mapStyle: StyleSpecification & { id: string }) {
|
save(mapStyle: StyleSpecification & { id: string }) {
|
||||||
mapStyle = style.ensureStyleValidity(mapStyle)
|
mapStyle = style.ensureStyleValidity(mapStyle)
|
||||||
const key = styleKey(mapStyle.id)
|
const key = styleKey(mapStyle.id)
|
||||||
window.localStorage.setItem(key, JSON.stringify(mapStyle))
|
|
||||||
window.localStorage.setItem(storageKeys.latest, 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
|
return mapStyle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user