@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.
@@ -1,4 +1,5 @@
1
1
  import { html as uHtml } from 'uhtml';
2
+ // SPDX-License-Identifier: Apache-2.0
2
3
  import GirafeHTMLElement from '../../base/GirafeHTMLElement.js';
3
4
  import { toLonLat } from 'ol/proj.js';
4
5
  import { getCenter } from 'ol/extent.js';
@@ -91,4 +91,4 @@ img:-moz-loading {
91
91
  .no-data span {
92
92
  display: block;
93
93
  flex-grow: 1;
94
- }
94
+ }
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.2605752849",
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.2605752849", "build":"2605752849", "date":"16/06/2026"}
1
+ {"version":"1.1.0-dev.2610902439", "build":"2610902439", "date":"18/06/2026"}
@@ -27,5 +27,5 @@
27
27
  "types": ["node"]
28
28
  },
29
29
  "include": ["src/**/*.ts"],
30
- "exclude": ["node_modules", "src/service-worker.ts", "src/service-worker.js"]
30
+ "exclude": ["node_modules", "src/tools/sw"]
31
31
  }
@@ -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}/service-worker.js`, dest: '' },
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
- sw.postMessage({ logLevel: config.general.logLevel });
174
- // Communicate oauth configuration to service-worker
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
- sw.postMessage({
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 database?;
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
- private openIndexedDB;
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
- database;
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
- window.addEventListener('offline', () => {
46
+ globalThis.addEventListener('offline', () => {
42
47
  this.state.isOffline = true;
43
48
  });
44
- window.addEventListener('online', () => {
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
- async openIndexedDB() {
59
- return new Promise((resolve, reject) => {
60
- console.debug('Opening IndexedDB');
61
- const request = indexedDB.open(this.dbCacheName, this.storeVersion);
62
- let timedOut = false;
63
- const timeoutId = setTimeout(() => {
64
- timedOut = true;
65
- console.debug('Timeout while opening IndexedDB');
66
- reject(new Error('Timeout while opening IndexedDB'));
67
- }, 3000);
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().then((blob) => {
184
- const transaction = this.database.transaction([this.tilesStoreName], 'readwrite');
185
- const store = transaction.objectStore(this.tilesStoreName);
186
- const index = store.index('url');
187
- const dbRequest = index.getKey(url);
188
- dbRequest.onsuccess = () => {
189
- let request;
190
- if (dbRequest.result) {
191
- const key = dbRequest.result;
192
- request = store.put({ url: url, data: blob }, key);
193
- }
194
- else {
195
- request = store.put({ url: url, data: blob });
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
- request.onsuccess = () => {
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 (!this.database) {
186
+ if (!database) {
214
187
  reject(new Error('Database is not initialized.'));
215
188
  return;
216
189
  }
217
- const transaction = this.database.transaction([this.tilesStoreName], 'readonly');
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 = this.database.transaction([storeName], 'readwrite');
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
- if (this.database === undefined) {
268
- this.database = await this.openIndexedDB();
269
- }
241
+ const database = await this.database();
270
242
  // Save bbox to the store
271
- const transaction = this.database.transaction([this.bboxStoreName], 'readwrite');
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
- if (this.database === undefined) {
283
- this.database = await this.openIndexedDB();
284
- }
254
+ const database = await this.database();
285
255
  // Read bbox for offline tiles
286
- const transaction = this.database.transaction([this.bboxStoreName], 'readonly');
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
+ }
@@ -6,7 +6,7 @@ export declare const MockConfig: {
6
6
  };
7
7
  languages: {
8
8
  translations: {
9
- fr: string[];
9
+ fr: never[];
10
10
  };
11
11
  defaultLanguage: string;
12
12
  };
@@ -7,7 +7,7 @@ export const MockConfig = {
7
7
  },
8
8
  languages: {
9
9
  translations: {
10
- fr: ['Mock/fr.json']
10
+ fr: []
11
11
  },
12
12
  defaultLanguage: 'fr'
13
13
  },
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
- }