@geogirafe/lib-geoportal 1.1.0-dev.2605752849 → 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/getdirections/component.js +1 -0
- package/components/treeview/treeviewroot/style.css +1 -1
- package/package.json +2 -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 +3 -1
- 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
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "GeoGirafe PSC",
|
|
6
6
|
"url": "https://doc.geogirafe.org"
|
|
7
7
|
},
|
|
8
|
-
"version": "1.1.0-dev.
|
|
8
|
+
"version": "1.1.0-dev.2610902439",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=20.19.0"
|
|
@@ -83,6 +83,7 @@
|
|
|
83
83
|
"cross-env": "10.1.0",
|
|
84
84
|
"eslint": "10.2.0",
|
|
85
85
|
"eslint-plugin-es-x": "9.6.0",
|
|
86
|
+
"fake-indexeddb": "6.2.5",
|
|
86
87
|
"fast-glob": "3.3.3",
|
|
87
88
|
"fs-extra": "11.3.4",
|
|
88
89
|
"globals": "17.4.0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.1.0-dev.
|
|
1
|
+
{"version":"1.1.0-dev.2610902439", "build":"2610902439", "date":"18/06/2026"}
|
package/templates/tsconfig.json
CHANGED
package/templates/vite.config.js
CHANGED
|
@@ -120,7 +120,7 @@ export default defineConfig(({ command, mode }) => {
|
|
|
120
120
|
{ src: `${cesiumSource}/Workers`, dest: cesiumBaseUrl },
|
|
121
121
|
{ src: `${cesiumSource}/Assets`, dest: cesiumBaseUrl },
|
|
122
122
|
{ src: `${cesiumSource}/Widgets`, dest: cesiumBaseUrl },
|
|
123
|
-
{ src: `${geogirafeSource}/
|
|
123
|
+
{ src: `${geogirafeSource}/tools/sw/*.js`, dest: '' },
|
|
124
124
|
{ src: `${geogirafeSource}/styles/*.css`, dest: 'styles/' },
|
|
125
125
|
{ src: `${geogirafeSource}/assets/*`, dest: '' },
|
|
126
126
|
{ src: `${geogirafeSource}/tools/auth/silentlogincallback.html`, dest: '' },
|
|
@@ -50,6 +50,7 @@ import SelectionToolComponent from '../../components/selectiontool/component.js'
|
|
|
50
50
|
import AdvancedFilterComponent from '../../components/advancedfilter/component.js';
|
|
51
51
|
import FixedDimensionComponent from '../../components/drawing/fixed-dimension/component.js';
|
|
52
52
|
import GetDirectionsArtifact from '../../components/getdirections/component.js';
|
|
53
|
+
import ServiceWorkerHelper from '../utils/swhelper.js';
|
|
53
54
|
import FeedbackButtonComponent from '../../components/feedbackbutton/component.js';
|
|
54
55
|
import AddBookmarkComponent from '../../components/addbookmark/component.js';
|
|
55
56
|
import DrawingToolbarComponent from '../../components/drawing-toolbar/component.js';
|
|
@@ -168,21 +169,29 @@ export default class GeoGirafeApp {
|
|
|
168
169
|
catch (err) {
|
|
169
170
|
throw new Error(`Service worker registration failed. ${err}`, { cause: err });
|
|
170
171
|
}
|
|
171
|
-
// Communicate logging configuration to service-worker
|
|
172
172
|
const config = this.context.configManager.Config;
|
|
173
|
-
|
|
174
|
-
// Communicate
|
|
173
|
+
const serviceWorkerHelper = new ServiceWorkerHelper(sw);
|
|
174
|
+
// Communicate configuration to service-worker
|
|
175
|
+
// NOTE REG : Important : we have to wait that the config has been correctly sent to the SW
|
|
176
|
+
// Otherwise the first requests can be sent without the correct auhentication.
|
|
175
177
|
const issuerConfig = config.oauth?.issuer ?? config.gmfauth;
|
|
176
178
|
const gmfConfig = config.oauth?.geomapfish ?? config.gmfauth;
|
|
177
179
|
if (issuerConfig && gmfConfig) {
|
|
178
180
|
const issuerHostname = new URL(issuerConfig.url).hostname;
|
|
179
181
|
const audience = [...issuerConfig.audience, issuerHostname];
|
|
180
|
-
|
|
182
|
+
const swConfig = {
|
|
183
|
+
logLevel: config.general.logLevel,
|
|
181
184
|
audience: audience,
|
|
182
185
|
audienceExcludedPaths: issuerConfig.audienceExcludedPaths,
|
|
183
186
|
authMode: gmfConfig.authMode,
|
|
187
|
+
alwaysSendCookies: issuerConfig.alwaysSendCookies,
|
|
184
188
|
refererPolicy: gmfConfig.refererPolicy
|
|
185
|
-
}
|
|
189
|
+
};
|
|
190
|
+
await serviceWorkerHelper.sendMessageToServiceWorker(swConfig);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// If no auth, we just update the log level, but we do not have to wait
|
|
194
|
+
sw.postMessage({ logLevel: config.general.logLevel });
|
|
186
195
|
}
|
|
187
196
|
await this.context.offlineManager.setServiceWorker(sw, storeVersion, dbCacheName);
|
|
188
197
|
await this.context.authManager.initialize(sw);
|
|
@@ -194,7 +203,7 @@ export default class GeoGirafeApp {
|
|
|
194
203
|
}
|
|
195
204
|
}
|
|
196
205
|
async waitForServiceWorkerActivation() {
|
|
197
|
-
const registration = await navigator.serviceWorker.register('service-worker.js');
|
|
206
|
+
const registration = await navigator.serviceWorker.register('service-worker.js', { type: 'module' });
|
|
198
207
|
if (registration.active) {
|
|
199
208
|
return registration.active;
|
|
200
209
|
}
|
|
@@ -191,6 +191,7 @@ declare class GirafeConfig {
|
|
|
191
191
|
checkSessionOnLoad: boolean;
|
|
192
192
|
audience: string[];
|
|
193
193
|
authMode: 'cookie';
|
|
194
|
+
alwaysSendCookies: boolean;
|
|
194
195
|
refererPolicy: ReferrerPolicy;
|
|
195
196
|
audienceExcludedPaths: string[];
|
|
196
197
|
};
|
|
@@ -205,6 +206,7 @@ declare class GirafeConfig {
|
|
|
205
206
|
checkSessionOnLoad: boolean;
|
|
206
207
|
audience: string[];
|
|
207
208
|
audienceExcludedPaths: string[];
|
|
209
|
+
alwaysSendCookies: boolean;
|
|
208
210
|
};
|
|
209
211
|
geomapfish: {
|
|
210
212
|
userInfoUrl: string;
|
|
@@ -358,6 +358,7 @@ class GirafeConfig {
|
|
|
358
358
|
loginRequired: config.gmfauth.loginRequired ?? false,
|
|
359
359
|
checkSessionOnLoad: config.gmfauth.checkSessionOnLoad ?? true,
|
|
360
360
|
authMode: 'cookie',
|
|
361
|
+
alwaysSendCookies: config.gmfauth.alwaysSendCookies ?? false,
|
|
361
362
|
refererPolicy: config.gmfauth.refererPolicy ?? 'strict-origin-when-cross-origin',
|
|
362
363
|
audienceExcludedPaths: config.gmfauth.audienceExcludedPaths ?? []
|
|
363
364
|
};
|
|
@@ -388,7 +389,8 @@ class GirafeConfig {
|
|
|
388
389
|
loginRequired: config.oauth.issuer.loginRequired ?? false,
|
|
389
390
|
checkSessionOnLoad: config.oauth.issuer.checkSessionOnLoad ?? false,
|
|
390
391
|
audience: config.oauth.issuer.audience,
|
|
391
|
-
audienceExcludedPaths: config.oauth.issuer.audienceExcludedPaths ?? []
|
|
392
|
+
audienceExcludedPaths: config.oauth.issuer.audienceExcludedPaths ?? [],
|
|
393
|
+
alwaysSendCookies: config.oauth.issuer.alwaysSendCookies ?? false
|
|
392
394
|
};
|
|
393
395
|
const geomapfishConfig = {
|
|
394
396
|
userInfoUrl: config.oauth.geomapfish.userInfoUrl,
|
|
@@ -42,7 +42,7 @@ class I18nManager extends GirafeSingleton {
|
|
|
42
42
|
this.loadingLanguagePromise = this.context.configManager.loadConfig().then(async () => {
|
|
43
43
|
if (this.context.configManager.Config?.languages.translations &&
|
|
44
44
|
language in this.context.configManager.Config.languages.translations) {
|
|
45
|
-
let mergedTranslations = {};
|
|
45
|
+
let mergedTranslations = this.translations[language] ?? {};
|
|
46
46
|
// Translations are loaded in the order defined in the list of files
|
|
47
47
|
// If an element is present in both results, the last value overwrite all the others
|
|
48
48
|
for (const url of this.context.configManager.Config.languages.translations[language]) {
|
package/tools/main.d.ts
CHANGED
|
@@ -104,6 +104,8 @@ export type { GgUserInteractionListener } from './state/userInteractionManager.j
|
|
|
104
104
|
export { default as UserInteractionManager } from './state/userInteractionManager.js';
|
|
105
105
|
export type { GgUserInteractionEvent } from './state/userinteractionevent.js';
|
|
106
106
|
export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
|
|
107
|
+
export type { SwState } from './sw/service-worker.tools';
|
|
108
|
+
export { swLog, SwHelper, IndexedDbHelper, CacheHelper } from './sw/service-worker.tools';
|
|
107
109
|
export { default as CustomThemesManager } from './themes/customthemesmanager.js';
|
|
108
110
|
export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
|
|
109
111
|
export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
|
package/tools/main.js
CHANGED
|
@@ -77,6 +77,7 @@ export { default as StateToggleManager } from './state/stateToggleManager.js';
|
|
|
77
77
|
export { default as StateManager } from './state/statemanager.js';
|
|
78
78
|
export { default as UserInteractionManager } from './state/userInteractionManager.js';
|
|
79
79
|
export { gGEventDependencies, isPrimaryPointerAction, isAlternateMouseClick, isMouseWheelClick } from './state/userinteractionevent.js';
|
|
80
|
+
export { swLog, SwHelper, IndexedDbHelper, CacheHelper } from './sw/service-worker.tools';
|
|
80
81
|
export { default as CustomThemesManager } from './themes/customthemesmanager.js';
|
|
81
82
|
export { default as ThemeFavoritesManager } from './themes/themefavoritesmanager.js';
|
|
82
83
|
export { DEFAULT_OPACITY, OPACITY_FOR_DEFAULT_BASEMAP } from './themes/themes-config.js';
|
|
@@ -3,14 +3,15 @@ import LayerWmts from '../../models/layers/layerwmts.js';
|
|
|
3
3
|
import { Extent } from 'ol/extent.js';
|
|
4
4
|
declare class OfflineManager extends GirafeSingleton {
|
|
5
5
|
private serviceWorker;
|
|
6
|
-
private
|
|
6
|
+
private readonly indexedDbHelper;
|
|
7
7
|
private get map();
|
|
8
8
|
private get state();
|
|
9
|
+
private database;
|
|
9
10
|
private totalLength;
|
|
10
11
|
private counter;
|
|
11
12
|
private progressCallback?;
|
|
12
|
-
private storeVersion
|
|
13
|
-
private dbCacheName
|
|
13
|
+
private storeVersion;
|
|
14
|
+
private dbCacheName;
|
|
14
15
|
private readonly tilesStoreName;
|
|
15
16
|
private readonly bboxStoreName;
|
|
16
17
|
private readonly vectorLayer;
|
|
@@ -19,7 +20,7 @@ declare class OfflineManager extends GirafeSingleton {
|
|
|
19
20
|
private registerEvents;
|
|
20
21
|
/** Exports all the WMTS tiles for the layers in parameter and store them to local cache */
|
|
21
22
|
exportWMTSTiles(bbox: Extent, wmtsLayers: LayerWmts[], progressCallback?: CallableFunction): Promise<void>;
|
|
22
|
-
|
|
23
|
+
upgradeIndexedDb(database: IDBDatabase): void;
|
|
23
24
|
/** The offline manager works with a ServiceWorker in charge of intercepting
|
|
24
25
|
* the fetch requests and read the data from the local cache if the application
|
|
25
26
|
* is offline. This method defines the servicework object to use.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import GirafeSingleton from '../../base/GirafeSingleton.js';
|
|
3
|
+
import { IndexedDbHelper } from '../sw/service-worker.tools';
|
|
3
4
|
import { Feature } from 'ol';
|
|
4
5
|
import VectorLayer from 'ol/layer/Vector.js';
|
|
5
6
|
import VectorSource from 'ol/source/Vector.js';
|
|
@@ -8,13 +9,17 @@ import Style from 'ol/style/Style.js';
|
|
|
8
9
|
import { Stroke } from 'ol/style.js';
|
|
9
10
|
class OfflineManager extends GirafeSingleton {
|
|
10
11
|
serviceWorker = null;
|
|
11
|
-
|
|
12
|
+
indexedDbHelper = new IndexedDbHelper();
|
|
12
13
|
get map() {
|
|
13
14
|
return this.context.mapManager.getMap();
|
|
14
15
|
}
|
|
15
16
|
get state() {
|
|
16
17
|
return this.context.stateManager.state;
|
|
17
18
|
}
|
|
19
|
+
async database() {
|
|
20
|
+
const database = await this.indexedDbHelper.openDb(this.dbCacheName, this.storeVersion, this.upgradeIndexedDb);
|
|
21
|
+
return database;
|
|
22
|
+
}
|
|
18
23
|
totalLength = 0;
|
|
19
24
|
counter = 0;
|
|
20
25
|
progressCallback;
|
|
@@ -38,10 +43,10 @@ class OfflineManager extends GirafeSingleton {
|
|
|
38
43
|
this.context.stateManager.state.isOffline = isOffline;
|
|
39
44
|
}
|
|
40
45
|
registerEvents() {
|
|
41
|
-
|
|
46
|
+
globalThis.addEventListener('offline', () => {
|
|
42
47
|
this.state.isOffline = true;
|
|
43
48
|
});
|
|
44
|
-
|
|
49
|
+
globalThis.addEventListener('online', () => {
|
|
45
50
|
this.state.isOffline = false;
|
|
46
51
|
});
|
|
47
52
|
this.context.stateManager.subscribe('isOffline', () => {
|
|
@@ -55,45 +60,16 @@ class OfflineManager extends GirafeSingleton {
|
|
|
55
60
|
const tileUrls = this.getAllTileUrls(bbox, wmtsLayers);
|
|
56
61
|
await this.fetchAndSaveTiles(tileUrls);
|
|
57
62
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
request.onerror = (event) => {
|
|
69
|
-
if (!timedOut) {
|
|
70
|
-
clearTimeout(timeoutId);
|
|
71
|
-
const message = event.target.error?.message;
|
|
72
|
-
console.debug(`IndexedDB could not be opened : ${message}`);
|
|
73
|
-
reject(new Error(message));
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
request.onsuccess = (event) => {
|
|
77
|
-
if (!timedOut) {
|
|
78
|
-
clearTimeout(timeoutId);
|
|
79
|
-
console.debug('IndexedDB is open');
|
|
80
|
-
resolve(event.target.result);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
request.onupgradeneeded = (event) => {
|
|
84
|
-
// Version migration is necessary.
|
|
85
|
-
// open the indexedDB and create the new structure
|
|
86
|
-
console.debug('Upgrading IndexedDB');
|
|
87
|
-
const database = event.target.result;
|
|
88
|
-
// First : a store for tiles
|
|
89
|
-
const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
|
|
90
|
-
tilesStore.createIndex('url', 'url', { unique: true });
|
|
91
|
-
// Second : A store for offline available bbox
|
|
92
|
-
database.createObjectStore('bbox', { autoIncrement: true });
|
|
93
|
-
console.debug('IndexedDB upgraded.');
|
|
94
|
-
resolve(database);
|
|
95
|
-
};
|
|
96
|
-
});
|
|
63
|
+
upgradeIndexedDb(database) {
|
|
64
|
+
// Version migration is necessary.
|
|
65
|
+
// open the indexedDB and create the new structure
|
|
66
|
+
console.debug('Upgrading IndexedDB');
|
|
67
|
+
// First : a store for tiles
|
|
68
|
+
const tilesStore = database.createObjectStore('tiles', { autoIncrement: true });
|
|
69
|
+
tilesStore.createIndex('url', 'url', { unique: true });
|
|
70
|
+
// Second : A store for offline available bbox
|
|
71
|
+
database.createObjectStore('bbox', { autoIncrement: true });
|
|
72
|
+
console.debug('IndexedDB upgraded.');
|
|
97
73
|
}
|
|
98
74
|
/** The offline manager works with a ServiceWorker in charge of intercepting
|
|
99
75
|
* the fetch requests and read the data from the local cache if the application
|
|
@@ -108,7 +84,6 @@ class OfflineManager extends GirafeSingleton {
|
|
|
108
84
|
this.serviceWorker = sw;
|
|
109
85
|
this.storeVersion = storeVersion;
|
|
110
86
|
this.dbCacheName = dbCacheName;
|
|
111
|
-
this.database = await this.openIndexedDB();
|
|
112
87
|
this.serviceWorker.postMessage({
|
|
113
88
|
storeVersion: this.storeVersion,
|
|
114
89
|
dbCacheName: this.dbCacheName,
|
|
@@ -165,9 +140,7 @@ class OfflineManager extends GirafeSingleton {
|
|
|
165
140
|
const iterator = tileUrls.values();
|
|
166
141
|
// Use 4 parallel workers to download tiles
|
|
167
142
|
this.counter = 0;
|
|
168
|
-
const workers = Array(4)
|
|
169
|
-
.fill(iterator)
|
|
170
|
-
.map((iterator) => this.doWork(iterator));
|
|
143
|
+
const workers = new Array(4).fill(iterator).map((iterator) => this.doWork(iterator));
|
|
171
144
|
Promise.allSettled(workers).then(() => {
|
|
172
145
|
console.debug('Everything downloaded.');
|
|
173
146
|
if (this.progressCallback) {
|
|
@@ -180,41 +153,41 @@ class OfflineManager extends GirafeSingleton {
|
|
|
180
153
|
for (const url of iterator) {
|
|
181
154
|
const response = await fetch(url);
|
|
182
155
|
if (response.ok) {
|
|
183
|
-
response.blob()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
156
|
+
const blob = await response.blob();
|
|
157
|
+
const transaction = (await this.database()).transaction([this.tilesStoreName], 'readwrite');
|
|
158
|
+
const store = transaction.objectStore(this.tilesStoreName);
|
|
159
|
+
const index = store.index('url');
|
|
160
|
+
const dbRequest = index.getKey(url);
|
|
161
|
+
dbRequest.onsuccess = () => {
|
|
162
|
+
let request;
|
|
163
|
+
if (dbRequest.result) {
|
|
164
|
+
const key = dbRequest.result;
|
|
165
|
+
request = store.put({ url: url, data: blob }, key);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
request = store.put({ url: url, data: blob });
|
|
169
|
+
}
|
|
170
|
+
request.onsuccess = () => {
|
|
171
|
+
if (this.progressCallback) {
|
|
172
|
+
this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
|
|
196
173
|
}
|
|
197
|
-
|
|
198
|
-
if (this.progressCallback) {
|
|
199
|
-
this.progressCallback(Math.round((this.counter * 100) / this.totalLength));
|
|
200
|
-
}
|
|
201
|
-
console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
|
|
202
|
-
};
|
|
203
|
-
request.onerror = () => {
|
|
204
|
-
console.error(`Error while saving tile ${url}`);
|
|
205
|
-
};
|
|
174
|
+
console.debug(`${this.counter++}/${this.totalLength} Tile ${url} added.`);
|
|
206
175
|
};
|
|
207
|
-
|
|
176
|
+
request.onerror = () => {
|
|
177
|
+
console.error(`Error while saving tile ${url}`);
|
|
178
|
+
};
|
|
179
|
+
};
|
|
208
180
|
}
|
|
209
181
|
}
|
|
210
182
|
}
|
|
211
183
|
async getTotalSizeMB() {
|
|
184
|
+
const database = await this.database();
|
|
212
185
|
return new Promise((resolve, reject) => {
|
|
213
|
-
if (!
|
|
186
|
+
if (!database) {
|
|
214
187
|
reject(new Error('Database is not initialized.'));
|
|
215
188
|
return;
|
|
216
189
|
}
|
|
217
|
-
const transaction =
|
|
190
|
+
const transaction = database.transaction([this.tilesStoreName], 'readonly');
|
|
218
191
|
const store = transaction.objectStore(this.tilesStoreName);
|
|
219
192
|
const request = store.openCursor();
|
|
220
193
|
let totalBytes = 0;
|
|
@@ -239,12 +212,13 @@ class OfflineManager extends GirafeSingleton {
|
|
|
239
212
|
});
|
|
240
213
|
}
|
|
241
214
|
async clearStore(storeName) {
|
|
215
|
+
const database = await this.database();
|
|
242
216
|
return new Promise((resolve, reject) => {
|
|
243
217
|
if (!this.database) {
|
|
244
218
|
reject(new Error('Database is not initialized.'));
|
|
245
219
|
return;
|
|
246
220
|
}
|
|
247
|
-
const transaction =
|
|
221
|
+
const transaction = database.transaction([storeName], 'readwrite');
|
|
248
222
|
const store = transaction.objectStore(storeName);
|
|
249
223
|
const request = store.clear();
|
|
250
224
|
request.onsuccess = () => {
|
|
@@ -264,11 +238,9 @@ class OfflineManager extends GirafeSingleton {
|
|
|
264
238
|
this.clearStore(this.bboxStoreName);
|
|
265
239
|
}
|
|
266
240
|
async saveBoundingBox(bbox) {
|
|
267
|
-
|
|
268
|
-
this.database = await this.openIndexedDB();
|
|
269
|
-
}
|
|
241
|
+
const database = await this.database();
|
|
270
242
|
// Save bbox to the store
|
|
271
|
-
const transaction =
|
|
243
|
+
const transaction = database.transaction([this.bboxStoreName], 'readwrite');
|
|
272
244
|
const store = transaction.objectStore(this.bboxStoreName);
|
|
273
245
|
const request = store.put(bbox);
|
|
274
246
|
request.onsuccess = () => {
|
|
@@ -279,11 +251,9 @@ class OfflineManager extends GirafeSingleton {
|
|
|
279
251
|
};
|
|
280
252
|
}
|
|
281
253
|
async displayBoundBoxes() {
|
|
282
|
-
|
|
283
|
-
this.database = await this.openIndexedDB();
|
|
284
|
-
}
|
|
254
|
+
const database = await this.database();
|
|
285
255
|
// Read bbox for offline tiles
|
|
286
|
-
const transaction =
|
|
256
|
+
const transaction = database.transaction([this.bboxStoreName], 'readonly');
|
|
287
257
|
const store = transaction.objectStore(this.bboxStoreName);
|
|
288
258
|
const request = store.getAll();
|
|
289
259
|
request.onsuccess = () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|
package/service-worker.js
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* To allow offline mode, the first 300 queries made by the application will be cached.
|
|
5
|
-
* We could have implemented a more specific cache with file extensions for example
|
|
6
|
-
* But then we should define exactly what should be cached.
|
|
7
|
-
* This can be quite complex, because we cannot just cache all images,
|
|
8
|
-
* as some of them are WMTS or WMS results and others are icons.
|
|
9
|
-
* So a simple solution here is to cache all the first queries that are done
|
|
10
|
-
* by the application when it starts. With this we should have all the necessary
|
|
11
|
-
* cache to be able to start the application in offline mode
|
|
12
|
-
*
|
|
13
|
-
* The WMTS tiles and other results coming from OGC-Services will be cached on demand
|
|
14
|
-
* with the OfflineManager component of the application.
|
|
15
|
-
*/
|
|
16
|
-
const sw = globalThis;
|
|
17
|
-
const currentOrigin = self.location.origin;
|
|
18
|
-
/**
|
|
19
|
-
* The service worker state needs to be persisted
|
|
20
|
-
* because when not using the app, the service worker can be paused by the browser
|
|
21
|
-
* and in this case it will be reinitialized with the next query
|
|
22
|
-
* with a base default empty state.
|
|
23
|
-
* See: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope
|
|
24
|
-
*/
|
|
25
|
-
const stateDbName = 'geogirafe-sw-state';
|
|
26
|
-
const stateStoreName = 'state';
|
|
27
|
-
const stateKey = 'config';
|
|
28
|
-
let initialized = false;
|
|
29
|
-
let state = {
|
|
30
|
-
storeVersion: 1,
|
|
31
|
-
dbCacheName: 'geogirafe-cache',
|
|
32
|
-
tilesStoreName: 'tiles',
|
|
33
|
-
logLevel: 'warning',
|
|
34
|
-
audience: [],
|
|
35
|
-
audienceExcludedPaths: [],
|
|
36
|
-
accessToken: undefined,
|
|
37
|
-
loginState: undefined,
|
|
38
|
-
authMode: undefined,
|
|
39
|
-
refererPolicy: undefined
|
|
40
|
-
};
|
|
41
|
-
function isLoggedIn() {
|
|
42
|
-
return state.loginState === 'loggedIn' || state.loginState === 'issuer.loggedIn';
|
|
43
|
-
}
|
|
44
|
-
const offlineTimeout = 3000; // Timeout in case of not reachable IndexedDB. (3 sec.)
|
|
45
|
-
const appCacheName = 'pages'; // Name of the cache for application pages
|
|
46
|
-
const maxCacheCount = 300; // Number of queries that should be cached by the service-worker for offline usage.
|
|
47
|
-
let cacheCount = 0; // Counter related to the max value above
|
|
48
|
-
let audienceExcludedPathPatterns = []; // list of regex pattern built from state.audienceExcludedPaths
|
|
49
|
-
sw.addEventListener('message', handleMessage);
|
|
50
|
-
sw.addEventListener('install', handleInstall);
|
|
51
|
-
sw.addEventListener('activate', handleActivate);
|
|
52
|
-
sw.addEventListener('fetch', (event) => {
|
|
53
|
-
if (event.request.mode === 'navigate') {
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
event.respondWith(handleFetchEvent(event));
|
|
57
|
-
});
|
|
58
|
-
function handleMessage(event) {
|
|
59
|
-
if (event.origin !== currentOrigin) {
|
|
60
|
-
console.warn(`Message ignored: origin not allowed (${event.origin})`);
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
const data = event.data;
|
|
64
|
-
if (data.logLevel) {
|
|
65
|
-
state.logLevel = data.logLevel;
|
|
66
|
-
log(`LogLevel changed: ${state.logLevel}`);
|
|
67
|
-
}
|
|
68
|
-
if (data.tilesStoreName) {
|
|
69
|
-
state.tilesStoreName = data.tilesStoreName;
|
|
70
|
-
log(`tilesStoreName changed: ${state.tilesStoreName}`);
|
|
71
|
-
}
|
|
72
|
-
if (data.storeVersion) {
|
|
73
|
-
state.storeVersion = data.storeVersion;
|
|
74
|
-
log(`storeVersion changed: ${state.storeVersion}`);
|
|
75
|
-
}
|
|
76
|
-
if (data.dbCacheName) {
|
|
77
|
-
state.dbCacheName = data.dbCacheName;
|
|
78
|
-
log(`dbCacheName changed: ${state.dbCacheName}`);
|
|
79
|
-
}
|
|
80
|
-
if (data.audience) {
|
|
81
|
-
state.audience = data.audience ?? [];
|
|
82
|
-
log(`audience changed: ${state.audience}`);
|
|
83
|
-
}
|
|
84
|
-
if (data.audienceExcludedPaths) {
|
|
85
|
-
state.audienceExcludedPaths = data.audienceExcludedPaths;
|
|
86
|
-
audienceExcludedPathPatterns = state.audienceExcludedPaths.map((str) => new RegExp(str));
|
|
87
|
-
log(`audienceExcludedPaths changed: ${state.audienceExcludedPaths}`);
|
|
88
|
-
}
|
|
89
|
-
if (data.access_token) {
|
|
90
|
-
state.accessToken = data.access_token;
|
|
91
|
-
log(`access_token changed: ${state.accessToken}`);
|
|
92
|
-
}
|
|
93
|
-
if (data.clear_access_token) {
|
|
94
|
-
state.accessToken = undefined;
|
|
95
|
-
log('access_token cleared');
|
|
96
|
-
}
|
|
97
|
-
if (data.authMode) {
|
|
98
|
-
state.authMode = data.authMode;
|
|
99
|
-
log(`authMode changed: ${state.authMode}`);
|
|
100
|
-
}
|
|
101
|
-
if (data.refererPolicy) {
|
|
102
|
-
state.refererPolicy = data.refererPolicy;
|
|
103
|
-
log(`refererPolicy changed: ${state.refererPolicy}`);
|
|
104
|
-
}
|
|
105
|
-
if (data.loginState) {
|
|
106
|
-
state.loginState = data.loginState;
|
|
107
|
-
log(`loginState changed: ${state.loginState}`);
|
|
108
|
-
}
|
|
109
|
-
event.waitUntil((async () => {
|
|
110
|
-
// Persist the service worker state before acknowledging the message.
|
|
111
|
-
await saveState();
|
|
112
|
-
if (data.messageId) {
|
|
113
|
-
const source = event.source;
|
|
114
|
-
if (source) {
|
|
115
|
-
source.postMessage({ messageId: data.messageId, status: 'ServiceWorker updated' });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
})());
|
|
119
|
-
}
|
|
120
|
-
function handleInstall() {
|
|
121
|
-
sw.skipWaiting();
|
|
122
|
-
log('Service worker installed');
|
|
123
|
-
}
|
|
124
|
-
function handleActivate(event) {
|
|
125
|
-
event.waitUntil((async () => {
|
|
126
|
-
if ('clients' in sw) {
|
|
127
|
-
await sw.clients.claim();
|
|
128
|
-
const clientsList = await sw.clients.matchAll({ type: 'window' });
|
|
129
|
-
for (const client of clientsList) {
|
|
130
|
-
client.navigate(client.url);
|
|
131
|
-
log('Page reloaded by the service worker.');
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
})());
|
|
135
|
-
}
|
|
136
|
-
async function handleFetchEvent(event) {
|
|
137
|
-
if (!initialized) {
|
|
138
|
-
await loadState();
|
|
139
|
-
}
|
|
140
|
-
const newRequest = getRequest(event.request);
|
|
141
|
-
let response = await fetchAndCache(newRequest);
|
|
142
|
-
// Use cache only for GET queries
|
|
143
|
-
if (isCacheAllowed(event.request)) {
|
|
144
|
-
if (!response) {
|
|
145
|
-
// Fetch was unsuccessful. We try to load the data from cache.
|
|
146
|
-
response = await loadFromCache(event.request);
|
|
147
|
-
}
|
|
148
|
-
if (!response) {
|
|
149
|
-
// Not found in cache. We try to load from IndexedDB.
|
|
150
|
-
response = await loadFromIndexedDB(event.request);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return response ?? new Response(null, { status: 503 });
|
|
154
|
-
}
|
|
155
|
-
function isCacheAllowed(request) {
|
|
156
|
-
if (request.method !== 'GET') {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
if (request.headers.get('Range')) {
|
|
160
|
-
// RANGE Request (for COG, FlatGeoBuf or GeoParquet for example)
|
|
161
|
-
return false;
|
|
162
|
-
}
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Execute a fetch for the query, and cache the result if successful
|
|
167
|
-
* Returns null if unsuccessful
|
|
168
|
-
*/
|
|
169
|
-
async function fetchAndCache(request) {
|
|
170
|
-
try {
|
|
171
|
-
const response = await fetch(request);
|
|
172
|
-
if (cacheCount < maxCacheCount && isCacheAllowed(request) && response.ok && response.type !== 'opaque') {
|
|
173
|
-
// Fetch was successful. We cache the result if necessary and return the response.
|
|
174
|
-
cacheCount++;
|
|
175
|
-
log(`SW ${cacheCount}/${maxCacheCount} caching ${request.url} for offline use.`);
|
|
176
|
-
const copy = response.clone();
|
|
177
|
-
const cache = await caches.open(appCacheName);
|
|
178
|
-
await cache.put(request, copy);
|
|
179
|
-
}
|
|
180
|
-
return response;
|
|
181
|
-
}
|
|
182
|
-
catch {
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function getRequest(request) {
|
|
187
|
-
if (request.mode === 'no-cors') {
|
|
188
|
-
return request; // Cannot do anything here, the no-cors queries cannot be modified
|
|
189
|
-
}
|
|
190
|
-
try {
|
|
191
|
-
const requestedUrl = new URL(request.url);
|
|
192
|
-
const hostname = requestedUrl.hostname;
|
|
193
|
-
const shouldExclude = audienceExcludedPathPatterns.some((pattern) => pattern.test(requestedUrl.pathname));
|
|
194
|
-
if (state.audience?.includes(hostname) && !shouldExclude) {
|
|
195
|
-
// Prepare headers
|
|
196
|
-
const headers = new Headers(request.headers);
|
|
197
|
-
if (state.authMode == 'token' && isLoggedIn() && state.accessToken) {
|
|
198
|
-
headers.set('Authorization', `Bearer ${state.accessToken}`);
|
|
199
|
-
}
|
|
200
|
-
// Prepare options
|
|
201
|
-
const fetchOptions = {
|
|
202
|
-
headers: headers,
|
|
203
|
-
credentials: isLoggedIn() && state.authMode === 'cookie' ? 'include' : 'omit',
|
|
204
|
-
referrer: request.referrer,
|
|
205
|
-
referrerPolicy: state.refererPolicy
|
|
206
|
-
};
|
|
207
|
-
return new Request(request, fetchOptions);
|
|
208
|
-
}
|
|
209
|
-
// Return the initial request
|
|
210
|
-
return request;
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
// In case of error, we return the initial request
|
|
214
|
-
log('Error while creating the request with authentication', error);
|
|
215
|
-
return request;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Try to load this request from local cache.
|
|
220
|
-
* Returns null if unsuccessful
|
|
221
|
-
*/
|
|
222
|
-
async function loadFromCache(request) {
|
|
223
|
-
const cachedResponse = await caches.match(request);
|
|
224
|
-
return cachedResponse || null;
|
|
225
|
-
}
|
|
226
|
-
/**
|
|
227
|
-
* Try to load this request from indexedDB.
|
|
228
|
-
* Returns null if unsuccessful
|
|
229
|
-
*/
|
|
230
|
-
async function loadFromIndexedDB(request) {
|
|
231
|
-
try {
|
|
232
|
-
const database = await openIndexedDB();
|
|
233
|
-
const transaction = database.transaction([state.tilesStoreName], 'readonly');
|
|
234
|
-
const store = transaction.objectStore(state.tilesStoreName);
|
|
235
|
-
const index = store.index('url');
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
const dbRequest = index.get(request.url);
|
|
238
|
-
dbRequest.onsuccess = () => {
|
|
239
|
-
if (dbRequest.result) {
|
|
240
|
-
const blob = dbRequest.result.data;
|
|
241
|
-
log('Tile found in cache.');
|
|
242
|
-
resolve(new Response(blob));
|
|
243
|
-
}
|
|
244
|
-
else {
|
|
245
|
-
log('Tile not found in cache.');
|
|
246
|
-
resolve(new Response(null, { status: 204 }));
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
dbRequest.onerror = () => {
|
|
250
|
-
reject(new Error('Error querying IndexedDB Store.'));
|
|
251
|
-
};
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Open IndexedDB
|
|
260
|
-
*/
|
|
261
|
-
async function openIndexedDB() {
|
|
262
|
-
return new Promise((resolve, reject) => {
|
|
263
|
-
const request = indexedDB.open(state.dbCacheName, state.storeVersion);
|
|
264
|
-
let timedOut = false;
|
|
265
|
-
const timeoutId = setTimeout(() => {
|
|
266
|
-
timedOut = true;
|
|
267
|
-
reject(new Error('SW Timeout while opening IndexedDB'));
|
|
268
|
-
}, offlineTimeout);
|
|
269
|
-
request.onerror = (event) => {
|
|
270
|
-
if (!timedOut) {
|
|
271
|
-
clearTimeout(timeoutId);
|
|
272
|
-
reject(event.target.error);
|
|
273
|
-
}
|
|
274
|
-
};
|
|
275
|
-
request.onsuccess = (event) => {
|
|
276
|
-
if (!timedOut) {
|
|
277
|
-
clearTimeout(timeoutId);
|
|
278
|
-
resolve(event.target.result);
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Open IndexedDB for the service worker state
|
|
285
|
-
*/
|
|
286
|
-
async function openStateDB() {
|
|
287
|
-
return new Promise((resolve, reject) => {
|
|
288
|
-
const request = indexedDB.open(stateDbName, 1);
|
|
289
|
-
request.onupgradeneeded = () => {
|
|
290
|
-
request.result.createObjectStore(stateStoreName);
|
|
291
|
-
};
|
|
292
|
-
request.onsuccess = () => resolve(request.result);
|
|
293
|
-
request.onerror = () => reject(request.error);
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
/**
|
|
297
|
-
* Saves the service worker state
|
|
298
|
-
*/
|
|
299
|
-
async function saveState() {
|
|
300
|
-
log('Save state');
|
|
301
|
-
const db = await openStateDB();
|
|
302
|
-
return new Promise((resolve, reject) => {
|
|
303
|
-
const tx = db.transaction(stateStoreName, 'readwrite');
|
|
304
|
-
tx.objectStore(stateStoreName).put(state, stateKey);
|
|
305
|
-
tx.oncomplete = () => resolve();
|
|
306
|
-
tx.onerror = () => reject(tx.error);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
/**
|
|
310
|
-
* Reloads the service worker state from indexDB
|
|
311
|
-
*/
|
|
312
|
-
async function loadState() {
|
|
313
|
-
log('Reload state');
|
|
314
|
-
const db = await openStateDB();
|
|
315
|
-
const loadedState = await new Promise((resolve, reject) => {
|
|
316
|
-
const tx = db.transaction(stateStoreName, 'readonly');
|
|
317
|
-
const req = tx.objectStore(stateStoreName).get(stateKey);
|
|
318
|
-
req.onsuccess = () => resolve(req.result);
|
|
319
|
-
req.onerror = () => reject(req.error);
|
|
320
|
-
});
|
|
321
|
-
if (loadedState) {
|
|
322
|
-
state = loadedState;
|
|
323
|
-
audienceExcludedPathPatterns = state.audienceExcludedPaths.map((str) => new RegExp(str));
|
|
324
|
-
}
|
|
325
|
-
initialized = true;
|
|
326
|
-
}
|
|
327
|
-
function log(str, error) {
|
|
328
|
-
if (state.logLevel === 'debug') {
|
|
329
|
-
console.debug(`SW: ${str}`, error ?? '');
|
|
330
|
-
}
|
|
331
|
-
}
|