@geogirafe/lib-geoportal 1.1.0-dev.2602404359 → 1.1.0-dev.2610902439
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.
- package/components/about/component.js +1 -1
- package/components/getdirections/component.js +1 -0
- package/components/prototypebanner/component.js +1 -1
- package/components/treeview/treeviewroot/style.css +1 -1
- package/package.json +4 -3
- package/templates/index.html +1 -1
- package/templates/public/about.json +1 -1
- package/templates/tsconfig.json +1 -1
- package/templates/vite.config.js +1 -1
- package/tools/app/geogirafeapp.js +15 -6
- package/tools/configuration/girafeconfig.d.ts +2 -0
- package/tools/configuration/girafeconfig.js +23 -21
- package/tools/i18n/i18nmanager.js +1 -1
- package/tools/main.d.ts +2 -0
- package/tools/main.js +1 -0
- package/tools/offline/offlinemanager.d.ts +5 -4
- package/tools/offline/offlinemanager.js +50 -80
- package/tools/sw/service-worker.d.ts +1 -0
- package/tools/sw/service-worker.js +94 -0
- package/tools/sw/service-worker.tools.d.ts +83 -0
- package/tools/sw/service-worker.tools.js +321 -0
- package/tools/tests/mockconfig.d.ts +1 -1
- package/tools/tests/mockconfig.js +1 -1
- package/service-worker.js +0 -331
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// NOTE REG: Here the extension ".js" MUST be present in the import,
|
|
3
|
+
// otherwise the service-worker won't be able to find the transpiled file
|
|
4
|
+
import { SwHelper, IndexedDbHelper, swLog, CacheHelper } from './service-worker.tools.js';
|
|
5
|
+
/**
|
|
6
|
+
* The service worker state needs to be persisted
|
|
7
|
+
* because when not using the app, the service worker can be paused by the browser
|
|
8
|
+
* and in this case it will be reinitialized with the next query
|
|
9
|
+
* with a base default empty state.
|
|
10
|
+
* See: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope
|
|
11
|
+
*/
|
|
12
|
+
const sw = globalThis;
|
|
13
|
+
const indexDbHelper = new IndexedDbHelper();
|
|
14
|
+
const cacheHelper = new CacheHelper();
|
|
15
|
+
// Initialize with default state
|
|
16
|
+
const swHelper = new SwHelper();
|
|
17
|
+
const swHelperReady = swHelper.initialize({
|
|
18
|
+
storeVersion: 1,
|
|
19
|
+
dbCacheName: 'geogirafe-cache',
|
|
20
|
+
tilesStoreName: 'tiles',
|
|
21
|
+
logLevel: 'warning',
|
|
22
|
+
audience: [],
|
|
23
|
+
audienceExcludedPaths: [],
|
|
24
|
+
accessToken: undefined,
|
|
25
|
+
loginState: undefined,
|
|
26
|
+
authMode: undefined,
|
|
27
|
+
alwaysSendCookies: false,
|
|
28
|
+
refererPolicy: undefined
|
|
29
|
+
});
|
|
30
|
+
sw.addEventListener('message', handleMessage);
|
|
31
|
+
sw.addEventListener('install', handleInstall);
|
|
32
|
+
sw.addEventListener('activate', handleActivate);
|
|
33
|
+
sw.addEventListener('fetch', (event) => {
|
|
34
|
+
if (event.request.mode === 'navigate') {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
event.respondWith(handleFetchEvent(event));
|
|
38
|
+
});
|
|
39
|
+
function handleMessage(event) {
|
|
40
|
+
if (!swHelper.isOriginAllowed(self.location.origin, event.origin)) {
|
|
41
|
+
console.warn(`Message ignored: origin not allowed (${event.origin})`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const data = event.data;
|
|
45
|
+
event.waitUntil((async () => {
|
|
46
|
+
await swHelperReady;
|
|
47
|
+
swHelper.updateState(data);
|
|
48
|
+
// Persist the service worker state before acknowledging the message.
|
|
49
|
+
await swHelper.saveState();
|
|
50
|
+
if (data.messageId) {
|
|
51
|
+
const source = event.source;
|
|
52
|
+
if (source) {
|
|
53
|
+
source.postMessage({ messageId: data.messageId, status: 'ServiceWorker updated' });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
})());
|
|
57
|
+
}
|
|
58
|
+
function handleInstall() {
|
|
59
|
+
sw.skipWaiting();
|
|
60
|
+
log('Service worker installed');
|
|
61
|
+
}
|
|
62
|
+
function handleActivate(event) {
|
|
63
|
+
event.waitUntil((async () => {
|
|
64
|
+
if ('clients' in sw) {
|
|
65
|
+
await swHelperReady;
|
|
66
|
+
await sw.clients.claim();
|
|
67
|
+
const clientsList = await sw.clients.matchAll({ type: 'window' });
|
|
68
|
+
for (const client of clientsList) {
|
|
69
|
+
client.navigate(client.url);
|
|
70
|
+
log('Page reloaded by the service worker.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
})());
|
|
74
|
+
}
|
|
75
|
+
async function handleFetchEvent(event) {
|
|
76
|
+
await swHelperReady;
|
|
77
|
+
const newRequest = swHelper.getRequest(event.request);
|
|
78
|
+
let response = await cacheHelper.fetchAndCache(swHelper.State, newRequest);
|
|
79
|
+
// Use cache only for GET queries
|
|
80
|
+
if (cacheHelper.isCacheAllowed(event.request)) {
|
|
81
|
+
if (!response) {
|
|
82
|
+
// Fetch was unsuccessful. We try to load the data from cache.
|
|
83
|
+
response = await cacheHelper.loadFromCache(event.request);
|
|
84
|
+
}
|
|
85
|
+
if (!response) {
|
|
86
|
+
// Not found in cache. We try to load from IndexedDB.
|
|
87
|
+
response = await indexDbHelper.loadFromIndexedDB(swHelper.State, event.request);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return response ?? new Response(null, { status: 503 });
|
|
91
|
+
}
|
|
92
|
+
function log(str, error) {
|
|
93
|
+
swLog(swHelper.State, str, error);
|
|
94
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type SwState = {
|
|
2
|
+
storeVersion: number;
|
|
3
|
+
dbCacheName: string;
|
|
4
|
+
tilesStoreName: string;
|
|
5
|
+
logLevel: string;
|
|
6
|
+
audience: string[];
|
|
7
|
+
audienceExcludedPaths: string[];
|
|
8
|
+
accessToken?: string;
|
|
9
|
+
loginState?: string;
|
|
10
|
+
authMode?: 'token' | 'cookie';
|
|
11
|
+
alwaysSendCookies: boolean;
|
|
12
|
+
refererPolicy?: ReferrerPolicy;
|
|
13
|
+
};
|
|
14
|
+
export declare function swLog(state: SwState, str: string, error?: Error): void;
|
|
15
|
+
export declare class SwHelper {
|
|
16
|
+
private state;
|
|
17
|
+
private readonly indexedDbHelper;
|
|
18
|
+
get State(): SwState;
|
|
19
|
+
private database;
|
|
20
|
+
private audienceExcludedPathPatterns;
|
|
21
|
+
private readonly stateDbName;
|
|
22
|
+
private readonly stateStoreName;
|
|
23
|
+
private readonly stateKey;
|
|
24
|
+
private initialized;
|
|
25
|
+
initialize(defaultState: SwState): Promise<void>;
|
|
26
|
+
isOriginAllowed(eventOrigin: string, origin: string): boolean;
|
|
27
|
+
private isLoggedIn;
|
|
28
|
+
updateState(data: any): void;
|
|
29
|
+
getRequest(request: Request): Request;
|
|
30
|
+
private log;
|
|
31
|
+
private refreshAudienceExcludedPathPatterns;
|
|
32
|
+
/**
|
|
33
|
+
* Saves the service worker state
|
|
34
|
+
*/
|
|
35
|
+
saveState(): Promise<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Reloads the service worker state from indexDB
|
|
38
|
+
*/
|
|
39
|
+
loadState(): Promise<SwState | null>;
|
|
40
|
+
private upgradeIndexedDb;
|
|
41
|
+
}
|
|
42
|
+
type indexedDbUpgradeFunction = (db: IDBDatabase) => void;
|
|
43
|
+
export declare class IndexedDbHelper {
|
|
44
|
+
private readonly timeout;
|
|
45
|
+
private readonly databases;
|
|
46
|
+
openDb(name: string, version: number, upgradeFunction?: indexedDbUpgradeFunction): Promise<IDBDatabase>;
|
|
47
|
+
private openDbInternal;
|
|
48
|
+
/**
|
|
49
|
+
* Try to load this request from indexedDB.
|
|
50
|
+
* Returns null if unsuccessful
|
|
51
|
+
*/
|
|
52
|
+
loadFromIndexedDB(state: SwState, request: Request): Promise<Response | null>;
|
|
53
|
+
}
|
|
54
|
+
export declare class CacheHelper {
|
|
55
|
+
/**
|
|
56
|
+
* To allow offline mode, the first 300 queries made by the application will be cached.
|
|
57
|
+
* We could have implemented a more specific cache with file extensions for example
|
|
58
|
+
* But then we should define exactly what should be cached.
|
|
59
|
+
* This can be quite complex, because we cannot just cache all images,
|
|
60
|
+
* as some of them are WMTS or WMS results and others are icons.
|
|
61
|
+
* So a simple solution here is to cache all the first queries that are done
|
|
62
|
+
* by the application when it starts. With this we should have all the necessary
|
|
63
|
+
* cache to be able to start the application in offline mode
|
|
64
|
+
*
|
|
65
|
+
* The WMTS tiles and other results coming from OGC-Services will be cached on demand
|
|
66
|
+
* with the OfflineManager component of the application.
|
|
67
|
+
*/
|
|
68
|
+
private readonly appCacheName;
|
|
69
|
+
private readonly maxCacheCount;
|
|
70
|
+
private cacheCount;
|
|
71
|
+
/**
|
|
72
|
+
* Execute a fetch for the query, and cache the result if successful
|
|
73
|
+
* Returns null if unsuccessful
|
|
74
|
+
*/
|
|
75
|
+
fetchAndCache(state: SwState, request: Request): Promise<Response | null>;
|
|
76
|
+
/**
|
|
77
|
+
* Try to load this request from local cache.
|
|
78
|
+
* Returns null if unsuccessful
|
|
79
|
+
*/
|
|
80
|
+
loadFromCache(request: Request): Promise<Response | null>;
|
|
81
|
+
isCacheAllowed(request: Request): boolean;
|
|
82
|
+
}
|
|
83
|
+
export {};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
export function swLog(state, str, error) {
|
|
2
|
+
if (state.logLevel === 'debug') {
|
|
3
|
+
console.debug(`SW: ${str}`, error ?? '');
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export class SwHelper {
|
|
7
|
+
state;
|
|
8
|
+
indexedDbHelper = new IndexedDbHelper();
|
|
9
|
+
get State() {
|
|
10
|
+
return this.state;
|
|
11
|
+
}
|
|
12
|
+
async database() {
|
|
13
|
+
const database = await this.indexedDbHelper.openDb(this.stateDbName, 1, this.upgradeIndexedDb.bind(this));
|
|
14
|
+
return database;
|
|
15
|
+
}
|
|
16
|
+
audienceExcludedPathPatterns = []; // list of regex pattern built from state.audienceExcludedPaths
|
|
17
|
+
stateDbName = 'geogirafe-sw-state';
|
|
18
|
+
stateStoreName = 'state';
|
|
19
|
+
stateKey = 'config';
|
|
20
|
+
initialized = false;
|
|
21
|
+
async initialize(defaultState) {
|
|
22
|
+
// If the state has already been initialized, do nothing
|
|
23
|
+
if (!this.initialized) {
|
|
24
|
+
this.state = defaultState;
|
|
25
|
+
let savedState = null;
|
|
26
|
+
try {
|
|
27
|
+
savedState = await this.loadState();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
this.log('No persisted state to load', error);
|
|
31
|
+
}
|
|
32
|
+
if (savedState) {
|
|
33
|
+
this.state = {
|
|
34
|
+
...defaultState,
|
|
35
|
+
...savedState
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
this.refreshAudienceExcludedPathPatterns();
|
|
39
|
+
this.initialized = true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
isOriginAllowed(eventOrigin, origin) {
|
|
43
|
+
if (eventOrigin !== origin) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
isLoggedIn() {
|
|
49
|
+
return this.state.loginState === 'loggedIn' || this.state.loginState === 'issuer.loggedIn';
|
|
50
|
+
}
|
|
51
|
+
updateState(data) {
|
|
52
|
+
if (data.logLevel) {
|
|
53
|
+
this.state.logLevel = data.logLevel;
|
|
54
|
+
this.log(`LogLevel changed: ${this.state.logLevel}`);
|
|
55
|
+
}
|
|
56
|
+
if (data.tilesStoreName) {
|
|
57
|
+
this.state.tilesStoreName = data.tilesStoreName;
|
|
58
|
+
this.log(`tilesStoreName changed: ${this.state.tilesStoreName}`);
|
|
59
|
+
}
|
|
60
|
+
if (data.storeVersion) {
|
|
61
|
+
this.state.storeVersion = data.storeVersion;
|
|
62
|
+
this.log(`storeVersion changed: ${this.state.storeVersion}`);
|
|
63
|
+
}
|
|
64
|
+
if (data.dbCacheName) {
|
|
65
|
+
this.state.dbCacheName = data.dbCacheName;
|
|
66
|
+
this.log(`dbCacheName changed: ${this.state.dbCacheName}`);
|
|
67
|
+
}
|
|
68
|
+
if (data.audience) {
|
|
69
|
+
this.state.audience = data.audience ?? [];
|
|
70
|
+
this.log(`audience changed: ${this.state.audience}`);
|
|
71
|
+
}
|
|
72
|
+
if (data.audienceExcludedPaths) {
|
|
73
|
+
this.state.audienceExcludedPaths = data.audienceExcludedPaths;
|
|
74
|
+
this.refreshAudienceExcludedPathPatterns();
|
|
75
|
+
this.log(`audienceExcludedPaths changed: ${this.state.audienceExcludedPaths}`);
|
|
76
|
+
}
|
|
77
|
+
if (data.access_token) {
|
|
78
|
+
this.state.accessToken = data.access_token;
|
|
79
|
+
this.log(`access_token changed`);
|
|
80
|
+
}
|
|
81
|
+
if (data.clear_access_token) {
|
|
82
|
+
this.state.accessToken = undefined;
|
|
83
|
+
this.log('access_token cleared');
|
|
84
|
+
}
|
|
85
|
+
if (data.authMode) {
|
|
86
|
+
this.state.authMode = data.authMode;
|
|
87
|
+
this.log(`authMode changed: ${this.state.authMode}`);
|
|
88
|
+
}
|
|
89
|
+
if (data.alwaysSendCookies !== undefined) {
|
|
90
|
+
this.state.alwaysSendCookies = data.alwaysSendCookies;
|
|
91
|
+
this.log(`alwaysSendCookies changed: ${this.state.alwaysSendCookies}`);
|
|
92
|
+
}
|
|
93
|
+
if (data.refererPolicy) {
|
|
94
|
+
this.state.refererPolicy = data.refererPolicy;
|
|
95
|
+
this.log(`refererPolicy changed: ${this.state.refererPolicy}`);
|
|
96
|
+
}
|
|
97
|
+
if (data.loginState) {
|
|
98
|
+
this.state.loginState = data.loginState;
|
|
99
|
+
this.log(`loginState changed: ${this.state.loginState}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
getRequest(request) {
|
|
103
|
+
if (request.mode === 'no-cors') {
|
|
104
|
+
return request; // Cannot do anything here, the no-cors queries cannot be modified
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const requestedUrl = new URL(request.url);
|
|
108
|
+
const hostname = requestedUrl.hostname;
|
|
109
|
+
// Prepare headers
|
|
110
|
+
const headers = new Headers(request.headers);
|
|
111
|
+
const shouldExclude = this.audienceExcludedPathPatterns.some((pattern) => pattern.test(requestedUrl.pathname));
|
|
112
|
+
const rightAudience = this.state.audience?.includes(hostname) && !shouldExclude;
|
|
113
|
+
if (rightAudience) {
|
|
114
|
+
// Token
|
|
115
|
+
const includeToken = this.state.authMode == 'token' && this.isLoggedIn() && this.state.accessToken;
|
|
116
|
+
if (includeToken) {
|
|
117
|
+
headers.set('Authorization', `Bearer ${this.state.accessToken}`);
|
|
118
|
+
}
|
|
119
|
+
// Cookie
|
|
120
|
+
const includeCookies = this.state.alwaysSendCookies || (this.isLoggedIn() && this.state.authMode === 'cookie');
|
|
121
|
+
// Prepare new request with authentication data
|
|
122
|
+
swLog(this.state, `including credentials for ${request.url} (rightAudience:${rightAudience}|includeCookies:${includeCookies}|includeToken:${includeToken})`);
|
|
123
|
+
const fetchOptions = {
|
|
124
|
+
headers: headers,
|
|
125
|
+
credentials: includeCookies ? 'include' : 'same-origin',
|
|
126
|
+
referrer: request.referrer,
|
|
127
|
+
referrerPolicy: this.state.refererPolicy
|
|
128
|
+
};
|
|
129
|
+
return new Request(request, fetchOptions);
|
|
130
|
+
}
|
|
131
|
+
// If there is no need for authentication data (token or cookie)
|
|
132
|
+
// We just return the original request
|
|
133
|
+
return request;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// In case of error, we return the initial request
|
|
137
|
+
this.log('Error while creating the request with authentication', error);
|
|
138
|
+
return request;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
log(str, error) {
|
|
142
|
+
swLog(this.state, str, error);
|
|
143
|
+
}
|
|
144
|
+
refreshAudienceExcludedPathPatterns() {
|
|
145
|
+
this.audienceExcludedPathPatterns = this.state.audienceExcludedPaths.map((str) => new RegExp(str));
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Saves the service worker state
|
|
149
|
+
*/
|
|
150
|
+
async saveState() {
|
|
151
|
+
this.log('Save state');
|
|
152
|
+
const db = await this.database();
|
|
153
|
+
return new Promise((resolve, reject) => {
|
|
154
|
+
const tx = db.transaction(this.stateStoreName, 'readwrite');
|
|
155
|
+
tx.objectStore(this.stateStoreName).put(this.state, this.stateKey);
|
|
156
|
+
tx.oncomplete = () => resolve();
|
|
157
|
+
tx.onerror = () => reject(tx.error);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reloads the service worker state from indexDB
|
|
162
|
+
*/
|
|
163
|
+
async loadState() {
|
|
164
|
+
this.log('Reload state');
|
|
165
|
+
const db = await this.database();
|
|
166
|
+
const loadedState = await new Promise((resolve, reject) => {
|
|
167
|
+
const tx = db.transaction(this.stateStoreName, 'readonly');
|
|
168
|
+
const req = tx.objectStore(this.stateStoreName).get(this.stateKey);
|
|
169
|
+
req.onsuccess = () => resolve(req.result);
|
|
170
|
+
req.onerror = () => reject(req.error);
|
|
171
|
+
});
|
|
172
|
+
if (loadedState) {
|
|
173
|
+
return loadedState;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
upgradeIndexedDb(database) {
|
|
178
|
+
database.createObjectStore(this.stateStoreName);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export class IndexedDbHelper {
|
|
182
|
+
timeout = 3000; // Timeout in case of not reachable IndexedDB. (3 sec.)
|
|
183
|
+
databases = {};
|
|
184
|
+
async openDb(name, version, upgradeFunction) {
|
|
185
|
+
const key = `${name}:${version}`;
|
|
186
|
+
let database = this.databases[key];
|
|
187
|
+
if (!database) {
|
|
188
|
+
database = await this.openDbInternal(name, version, upgradeFunction);
|
|
189
|
+
this.databases[key] = database;
|
|
190
|
+
}
|
|
191
|
+
return database;
|
|
192
|
+
}
|
|
193
|
+
async openDbInternal(name, version, upgradeFunction) {
|
|
194
|
+
return new Promise((resolve, reject) => {
|
|
195
|
+
const request = indexedDB.open(name, version);
|
|
196
|
+
let timedOut = false;
|
|
197
|
+
const timeoutId = setTimeout(() => {
|
|
198
|
+
timedOut = true;
|
|
199
|
+
reject(new Error('Timeout while opening IndexedDB'));
|
|
200
|
+
}, this.timeout);
|
|
201
|
+
request.onerror = (event) => {
|
|
202
|
+
if (!timedOut) {
|
|
203
|
+
clearTimeout(timeoutId);
|
|
204
|
+
reject(event.target.error);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
request.onsuccess = (event) => {
|
|
208
|
+
if (!timedOut) {
|
|
209
|
+
clearTimeout(timeoutId);
|
|
210
|
+
resolve(event.target.result);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
request.onupgradeneeded = (event) => {
|
|
214
|
+
const database = event.target.result;
|
|
215
|
+
const transaction = request.transaction;
|
|
216
|
+
if (upgradeFunction) {
|
|
217
|
+
// Call upgrade function
|
|
218
|
+
upgradeFunction(database);
|
|
219
|
+
console.debug('IndexedDB upgraded.');
|
|
220
|
+
}
|
|
221
|
+
if (transaction) {
|
|
222
|
+
transaction.oncomplete = () => {
|
|
223
|
+
resolve(database);
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Try to load this request from indexedDB.
|
|
231
|
+
* Returns null if unsuccessful
|
|
232
|
+
*/
|
|
233
|
+
async loadFromIndexedDB(state, request) {
|
|
234
|
+
try {
|
|
235
|
+
const database = await this.openDb(state.dbCacheName, state.storeVersion);
|
|
236
|
+
const transaction = database.transaction([state.tilesStoreName], 'readonly');
|
|
237
|
+
const store = transaction.objectStore(state.tilesStoreName);
|
|
238
|
+
const index = store.index('url');
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
const dbRequest = index.get(request.url);
|
|
241
|
+
dbRequest.onsuccess = () => {
|
|
242
|
+
if (dbRequest.result) {
|
|
243
|
+
const blob = dbRequest.result.data;
|
|
244
|
+
swLog(state, 'Tile found in cache.');
|
|
245
|
+
resolve(new Response(blob));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
swLog(state, 'Tile not found in cache.');
|
|
249
|
+
resolve(new Response(null, { status: 204 }));
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
dbRequest.onerror = () => {
|
|
253
|
+
reject(new Error('Error querying IndexedDB Store.'));
|
|
254
|
+
};
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
export class CacheHelper {
|
|
263
|
+
/**
|
|
264
|
+
* To allow offline mode, the first 300 queries made by the application will be cached.
|
|
265
|
+
* We could have implemented a more specific cache with file extensions for example
|
|
266
|
+
* But then we should define exactly what should be cached.
|
|
267
|
+
* This can be quite complex, because we cannot just cache all images,
|
|
268
|
+
* as some of them are WMTS or WMS results and others are icons.
|
|
269
|
+
* So a simple solution here is to cache all the first queries that are done
|
|
270
|
+
* by the application when it starts. With this we should have all the necessary
|
|
271
|
+
* cache to be able to start the application in offline mode
|
|
272
|
+
*
|
|
273
|
+
* The WMTS tiles and other results coming from OGC-Services will be cached on demand
|
|
274
|
+
* with the OfflineManager component of the application.
|
|
275
|
+
*/
|
|
276
|
+
appCacheName = 'pages'; // Name of the cache for application pages
|
|
277
|
+
maxCacheCount = 300; // Number of queries that should be cached by the service-worker for offline usage.
|
|
278
|
+
cacheCount = 0; // Counter related to the max value above
|
|
279
|
+
/**
|
|
280
|
+
* Execute a fetch for the query, and cache the result if successful
|
|
281
|
+
* Returns null if unsuccessful
|
|
282
|
+
*/
|
|
283
|
+
async fetchAndCache(state, request) {
|
|
284
|
+
try {
|
|
285
|
+
const response = await fetch(request);
|
|
286
|
+
if (this.cacheCount < this.maxCacheCount &&
|
|
287
|
+
this.isCacheAllowed(request) &&
|
|
288
|
+
response.ok &&
|
|
289
|
+
response.type !== 'opaque') {
|
|
290
|
+
// Fetch was successful. We cache the result if necessary and return the response.
|
|
291
|
+
this.cacheCount++;
|
|
292
|
+
swLog(state, `${this.cacheCount}/${this.maxCacheCount} caching ${request.url} for offline use.`);
|
|
293
|
+
const copy = response.clone();
|
|
294
|
+
const cache = await caches.open(this.appCacheName);
|
|
295
|
+
await cache.put(request, copy);
|
|
296
|
+
}
|
|
297
|
+
return response;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Try to load this request from local cache.
|
|
305
|
+
* Returns null if unsuccessful
|
|
306
|
+
*/
|
|
307
|
+
async loadFromCache(request) {
|
|
308
|
+
const cachedResponse = await caches.match(request);
|
|
309
|
+
return cachedResponse || null;
|
|
310
|
+
}
|
|
311
|
+
isCacheAllowed(request) {
|
|
312
|
+
if (request.method !== 'GET') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
if (request.headers.get('Range')) {
|
|
316
|
+
// RANGE Request (for COG, FlatGeoBuf or GeoParquet for example)
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|