underpost 2.85.1 → 2.89.0

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.
Files changed (53) hide show
  1. package/.env.development +2 -1
  2. package/.env.production +2 -1
  3. package/.env.test +2 -1
  4. package/.github/workflows/release.cd.yml +3 -3
  5. package/.vscode/zed.keymap.json +22 -0
  6. package/README.md +3 -3
  7. package/bin/build.js +8 -10
  8. package/bin/deploy.js +4 -2
  9. package/bin/file.js +4 -0
  10. package/bin/vs.js +4 -4
  11. package/cli.md +16 -11
  12. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  13. package/manifests/deployment/dd-test-development/deployment.yaml +50 -50
  14. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  15. package/package.json +2 -2
  16. package/src/api/file/file.service.js +29 -3
  17. package/src/cli/baremetal.js +4 -5
  18. package/src/cli/deploy.js +26 -4
  19. package/src/cli/index.js +8 -3
  20. package/src/cli/repository.js +42 -45
  21. package/src/cli/run.js +217 -48
  22. package/src/client/components/core/AgGrid.js +42 -3
  23. package/src/client/components/core/CommonJs.js +5 -0
  24. package/src/client/components/core/Css.js +95 -48
  25. package/src/client/components/core/CssCore.js +0 -1
  26. package/src/client/components/core/LoadingAnimation.js +2 -2
  27. package/src/client/components/core/Logger.js +2 -9
  28. package/src/client/components/core/Modal.js +22 -14
  29. package/src/client/components/core/ObjectLayerEngine.js +300 -9
  30. package/src/client/components/core/ObjectLayerEngineModal.js +686 -148
  31. package/src/client/components/core/ObjectLayerEngineViewer.js +1061 -0
  32. package/src/client/components/core/Pagination.js +15 -5
  33. package/src/client/components/core/Router.js +5 -1
  34. package/src/client/components/core/SocketIo.js +5 -1
  35. package/src/client/components/core/Translate.js +4 -0
  36. package/src/client/components/core/Worker.js +8 -1
  37. package/src/client/services/default/default.management.js +86 -16
  38. package/src/client/sw/default.sw.js +193 -97
  39. package/src/client.dev.js +1 -1
  40. package/src/db/mariadb/MariaDB.js +2 -2
  41. package/src/index.js +1 -1
  42. package/src/proxy.js +1 -1
  43. package/src/runtime/express/Express.js +4 -1
  44. package/src/server/auth.js +2 -1
  45. package/src/server/client-build.js +57 -2
  46. package/src/server/conf.js +132 -15
  47. package/src/server/object-layer.js +44 -0
  48. package/src/server/proxy.js +53 -26
  49. package/src/server/start.js +25 -3
  50. package/src/server/tls.js +1 -1
  51. package/src/ws/IoInterface.js +2 -3
  52. package/AUTHORS.md +0 -21
  53. package/src/server/network.js +0 -72
@@ -10,12 +10,13 @@ class AgPagination extends HTMLElement {
10
10
  this._limit = parseInt(queryParams.limit, 10) || 10;
11
11
  this._totalPages = 1;
12
12
  this._totalItems = 0;
13
+ this._limitOptions = [10, 20, 50, 100]; // Default options
13
14
  this.handlePageChange = this.handlePageChange.bind(this);
14
15
  this.handleLimitChange = this.handleLimitChange.bind(this);
15
16
  }
16
17
 
17
18
  static get observedAttributes() {
18
- return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit'];
19
+ return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit', 'limit-options'];
19
20
  }
20
21
 
21
22
  attributeChangedCallback(name, oldValue, newValue) {
@@ -35,6 +36,16 @@ class AgPagination extends HTMLElement {
35
36
  case 'limit':
36
37
  this._limit = parseInt(newValue, 10) || this._limit;
37
38
  break;
39
+ case 'limit-options':
40
+ try {
41
+ const parsed = JSON.parse(newValue);
42
+ if (Array.isArray(parsed) && parsed.length > 0) {
43
+ this._limitOptions = parsed.map((v) => parseInt(v, 10)).filter((v) => !isNaN(v) && v > 0);
44
+ }
45
+ } catch (e) {
46
+ console.warn('Invalid limit-options format, using defaults');
47
+ }
48
+ break;
38
49
  }
39
50
  this.update();
40
51
  }
@@ -133,6 +144,8 @@ class AgPagination extends HTMLElement {
133
144
  }
134
145
 
135
146
  render() {
147
+ const limitOptionsHtml = this._limitOptions.map((value) => `<option value="${value}">${value}</option>`).join('');
148
+
136
149
  this.shadowRoot.innerHTML = html`
137
150
  <style>
138
151
  :host {
@@ -193,10 +206,7 @@ class AgPagination extends HTMLElement {
193
206
  <button id="next-page">Next</button>
194
207
  <button id="last-page">Last</button>
195
208
  <select id="limit-selector">
196
- <option value="10">10</option>
197
- <option value="20">20</option>
198
- <option value="50">50</option>
199
- <option value="100">100</option>
209
+ ${limitOptionsHtml}
200
210
  </select>
201
211
  `;
202
212
  }
@@ -300,7 +300,11 @@ const setQueryParams = (newParams, options = { replace: true }) => {
300
300
 
301
301
  const newPath = url.pathname + url.search + url.hash;
302
302
 
303
- history.pushState(history.state, '', newPath);
303
+ if (options.replace) {
304
+ history.replaceState(history.state, '', newPath);
305
+ } else {
306
+ history.pushState(history.state, '', newPath);
307
+ }
304
308
  };
305
309
 
306
310
  export {
@@ -35,8 +35,12 @@ const SocketIo = {
35
35
  // forceNew: true,
36
36
  // reconnectionAttempts: 'Infinity',
37
37
  // timeout: 10000,
38
- // withCredentials: true,
39
38
  // autoConnect: 5000,
39
+ // Custom auth socket io credentials:
40
+ withCredentials: true,
41
+ extraHeaders: {
42
+ // "my-custom-header": "abcd"
43
+ },
40
44
  transports: ['websocket', 'polling', 'flashsocket'],
41
45
  };
42
46
  // logger.error(`connect options:`, JSON.stringify(connectOptions, null, 4));
@@ -516,6 +516,10 @@ const TranslateCore = {
516
516
  en: 'Cron Management',
517
517
  es: 'Gestion de cron jobs',
518
518
  };
519
+ Translate.Data['success-reload-data'] = {
520
+ en: 'Data reloaded successfully.',
521
+ es: 'Datos recargados con éxito.',
522
+ };
519
523
  },
520
524
  };
521
525
 
@@ -53,15 +53,22 @@ class PwaWorker {
53
53
  */
54
54
  constructor() {
55
55
  this.title = `${s('title').textContent}`;
56
+ if (!window.renderPayload.dev) {
57
+ console.log = () => null;
58
+ console.error = () => null;
59
+ console.info = () => null;
60
+ console.warn = () => null;
61
+ }
56
62
  }
57
63
 
58
64
  /**
59
65
  * Checks if the application is running in development mode (localhost or 127.0.0.1).
66
+ * @method devMode
60
67
  * @memberof PwaWorker
61
68
  * @returns {boolean} True if in development mode.
62
69
  */
63
70
  devMode() {
64
- return location.origin.match('localhost') || location.origin.match('127.0.0.1');
71
+ return window.renderPayload.dev || location.origin.match('localhost') || location.origin.match('127.0.0.1');
65
72
  }
66
73
 
67
74
  /**
@@ -18,7 +18,7 @@ const DefaultOptions = {
18
18
  serviceId: 'default-management',
19
19
  entity: 'default',
20
20
  columnDefs: [
21
- { field: '0', headerName: '0' },
21
+ { field: '0', headerName: '0', cellClassRules: { 'row-new-highlight': (params) => true } },
22
22
  { field: '1', headerName: '1' },
23
23
  { field: '2', headerName: '2' },
24
24
  { field: 'createdAt', headerName: 'createdAt', cellDataType: 'date', editable: false },
@@ -29,6 +29,10 @@ const DefaultOptions = {
29
29
  permissions: {
30
30
  add: true,
31
31
  remove: true,
32
+ reload: true,
33
+ },
34
+ paginationOptions: {
35
+ limitOptions: [10, 20, 50, 100],
32
36
  },
33
37
  };
34
38
 
@@ -66,25 +70,36 @@ const DefaultManagement = {
66
70
  paginationComp.setAttribute('current-page', this.Tokens[id].page);
67
71
  paginationComp.setAttribute('total-pages', this.Tokens[id].totalPages);
68
72
  paginationComp.setAttribute('total-items', this.Tokens[id].total);
73
+ setTimeout(async () => {
74
+ if (DefaultManagement.Tokens[id].readyRowDataEvent)
75
+ for (const event of Object.keys(DefaultManagement.Tokens[id].readyRowDataEvent))
76
+ await DefaultManagement.Tokens[id].readyRowDataEvent[event](rowDataScope);
77
+ }, 1);
78
+ }
79
+ },
80
+ refreshTable: async function (id) {
81
+ const gridApi = AgGrid.grids[this.Tokens[id].gridId];
82
+ if (gridApi) {
83
+ // Use refreshCells with change detection for optimal performance
84
+ // This is preferred over redrawRows() as it only updates changed cells
85
+ gridApi.refreshCells({
86
+ force: false, // Use change detection - only refresh cells whose values have changed
87
+ suppressFlash: false, // Show flash animation for changed cells (requires enableCellChangeFlash)
88
+ });
69
89
  }
70
90
  },
71
91
  RenderTable: async function (options = DefaultOptions) {
72
92
  if (!options) options = DefaultOptions;
73
- const { serviceId, columnDefs, entity, defaultColKeyFocus, ServiceProvider, permissions } = options;
93
+ const { serviceId, columnDefs, entity, defaultColKeyFocus, ServiceProvider, permissions, paginationOptions } =
94
+ options;
74
95
  logger.info('DefaultManagement RenderTable', options);
75
96
  const id = options?.idModal ? options.idModal : getId(this.Tokens, `${serviceId}-`);
76
97
  const gridId = `${serviceId}-grid-${id}`;
77
98
  const queryParams = getQueryParams();
78
99
  const page = parseInt(queryParams.page) || 1;
79
- const limit = parseInt(queryParams.limit) || 10;
80
- this.Tokens[id] = {
81
- ...options,
82
- gridId,
83
- page,
84
- limit,
85
- total: 0,
86
- totalPages: 1,
87
- };
100
+ const defaultLimit = paginationOptions?.limitOptions?.[0] || 10;
101
+ const limit = parseInt(queryParams.limit) || defaultLimit;
102
+ this.Tokens[id] = { ...this.Tokens[id], ...options, gridId, page, limit, total: 0, totalPages: 1 };
88
103
 
89
104
  setQueryParams({ page, limit });
90
105
  setTimeout(async () => {
@@ -107,7 +122,7 @@ const DefaultManagement = {
107
122
  label: html`<div class="abs center">
108
123
  <i class="fas fa-times"></i>
109
124
  </div> `,
110
- class: `in fll section-mp management-table-btn-mini management-table-btn-remove-${id}-${cellRenderId}`,
125
+ class: `in fll section-mp management-table-btn-mini management-table-btn-remove-${id}-${cellRenderId} ${!params.data._id ? 'hide' : ''}`,
111
126
  })}`;
112
127
  setTimeout(() => {
113
128
  EventsUI.onClick(
@@ -191,9 +206,16 @@ const DefaultManagement = {
191
206
  // }
192
207
  // }
193
208
  s(`.management-table-btn-save-${id}`).onclick = () => {
209
+ s(`.management-table-btn-save-${id}`).classList.add('hide');
210
+ // s(`.management-table-btn-stop-${id}`).classList.add('hide');
211
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
212
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
213
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
194
214
  AgGrid.grids[gridId].stopEditing();
195
215
  };
196
216
  EventsUI.onClick(`.management-table-btn-add-${id}`, async () => {
217
+ if (options.customEvent && options.customEvent.add) return await options.customEvent.add();
218
+
197
219
  const rowObj = {};
198
220
  for (const def of columnDefs) {
199
221
  rowObj[def.field] = '';
@@ -260,6 +282,11 @@ const DefaultManagement = {
260
282
  // }
261
283
 
262
284
  setTimeout(() => {
285
+ s(`.management-table-btn-save-${id}`).classList.remove('hide');
286
+ // s(`.management-table-btn-stop-${id}`).classList.remove('hide');
287
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
288
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
289
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
263
290
  AgGrid.grids[gridId].startEditingCell({
264
291
  rowIndex: 0,
265
292
  colKey: defaultColKeyFocus,
@@ -268,6 +295,15 @@ const DefaultManagement = {
268
295
  });
269
296
  });
270
297
  });
298
+
299
+ EventsUI.onClick(`.management-table-btn-stop-${id}`, async () => {
300
+ s(`.management-table-btn-save-${id}`).classList.add('hide');
301
+ // s(`.management-table-btn-stop-${id}`).classList.add('hide');
302
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
303
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
304
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
305
+ AgGrid.grids[gridId].stopEditing();
306
+ });
271
307
  EventsUI.onClick(`.management-table-btn-clean-${id}`, async () => {
272
308
  const confirmResult = await Modal.RenderConfirm(
273
309
  {
@@ -292,6 +328,26 @@ const DefaultManagement = {
292
328
  DefaultManagement.loadTable(id);
293
329
  }
294
330
  });
331
+ EventsUI.onClick(`.management-table-btn-reload-${id}`, async () => {
332
+ try {
333
+ // Reload data from server
334
+ await DefaultManagement.loadTable(id);
335
+
336
+ // Other option: Refresh cells to update UI
337
+ // DefaultManagement.refreshTable(id);
338
+
339
+ NotificationManager.Push({
340
+ html: Translate.Render('success-reload-data') || 'Data reloaded successfully',
341
+ status: 'success',
342
+ });
343
+ } catch (error) {
344
+ NotificationManager.Push({
345
+ html: error.message || 'Error reloading data',
346
+ status: 'error',
347
+ });
348
+ } finally {
349
+ }
350
+ });
295
351
  s(`#ag-pagination-${gridId}`).addEventListener('page-change', async (event) => {
296
352
  const token = DefaultManagement.Tokens[id];
297
353
  token.page = event.detail.page;
@@ -318,7 +374,7 @@ const DefaultManagement = {
318
374
  }
319
375
  };
320
376
  }, 1);
321
- return html`<div class="fl">
377
+ return html`<div class="fl management-table-toolbar">
322
378
  ${await BtnIcon.Render({
323
379
  class: `in fll section-mp management-table-btn-mini management-table-btn-add-${id} ${
324
380
  permissions.add ? '' : 'hide'
@@ -327,12 +383,15 @@ const DefaultManagement = {
327
383
  type: 'button',
328
384
  })}
329
385
  ${await BtnIcon.Render({
330
- class: `in fll section-mp management-table-btn-mini management-table-btn-save-${id} ${
331
- permissions.add ? '' : 'hide'
332
- }`,
386
+ class: `in fll section-mp management-table-btn-mini management-table-btn-save-${id} hide`,
333
387
  label: html`<div class="abs center btn-save-${id}-label"><i class="fas fa-save"></i></div> `,
334
388
  type: 'button',
335
389
  })}
390
+ ${await BtnIcon.Render({
391
+ class: `in fll section-mp management-table-btn-mini management-table-btn-stop-${id} hide`,
392
+ label: html`<div class="abs center btn-save-${id}-label"><i class="fa-solid fa-rectangle-xmark"></i></div> `,
393
+ type: 'button',
394
+ })}
336
395
  ${await BtnIcon.Render({
337
396
  class: `in fll section-mp management-table-btn-mini management-table-btn-clean-${id} ${
338
397
  permissions.remove ? '' : 'hide'
@@ -340,12 +399,21 @@ const DefaultManagement = {
340
399
  label: html`<div class="abs center btn-clean-${id}-label"><i class="fas fa-broom"></i></div> `,
341
400
  type: 'button',
342
401
  })}
402
+ ${await BtnIcon.Render({
403
+ class: `in fll section-mp management-table-btn-mini management-table-btn-reload-${id} ${
404
+ permissions.reload ? '' : 'hide'
405
+ }`,
406
+ label: html`<div class="abs center btn-reload-${id}-label"><i class="fas fa-sync-alt"></i></div> `,
407
+ type: 'button',
408
+ })}
343
409
  </div>
344
410
  <div class="in section-mp">
345
411
  ${await AgGrid.Render({
346
412
  id: gridId,
347
413
  parentModal: options.idModal,
348
414
  usePagination: true,
415
+ paginationOptions,
416
+ customHeightOffset: !permissions.add && !permissions.remove && !permissions.reload ? 50 : 0,
349
417
  darkTheme,
350
418
  gridOptions: {
351
419
  defaultColDef: {
@@ -425,6 +493,7 @@ const DefaultManagement = {
425
493
  // rowNode.setData(newRow);
426
494
  // }, 2000);
427
495
  }
496
+ s(`.management-table-btn-save-${id}`).click();
428
497
  }
429
498
  } else {
430
499
  const body = event.data ? event.data : {};
@@ -441,6 +510,7 @@ const DefaultManagement = {
441
510
  }
442
511
  }
443
512
  },
513
+ ...(options.gridOptions ? options.gridOptions : undefined),
444
514
  },
445
515
  })}
446
516
  </div>`;
@@ -1,108 +1,204 @@
1
- const PRE_CACHED_RESOURCES = self.renderPayload?.PRE_CACHED_RESOURCES ? self.renderPayload.PRE_CACHED_RESOURCES : [];
2
- const CACHE_NAME = self.renderPayload?.CACHE_NAME ? self.renderPayload.CACHE_NAME : 'app-cache';
3
- const PROXY_PATH = self.renderPayload?.PROXY_PATH ? self.renderPayload.PROXY_PATH : '/';
4
- self.addEventListener('install', (event) => {
5
- // Activate right away
6
- self.skipWaiting();
7
-
8
- event.waitUntil(
9
- (async () => {
10
- // Open the app's cache.
11
- const cache = await caches.open(CACHE_NAME);
12
- // Cache all static resources.
13
- try {
14
- await cache.addAll(PRE_CACHED_RESOURCES);
15
- } catch (error) {
16
- console.error(error);
17
- }
18
- // for (const cacheKey of PRE_CACHED_RESOURCES) {
19
- // try {
20
- // await cache.add(cacheKey);
21
- // } catch (error) {
22
- // console.error(error, cacheKey);
23
- // }
24
- // }
25
- })(),
26
- );
27
- });
28
-
29
- self.addEventListener('activate', (event) => {
30
- event.waitUntil(
31
- (async () => {
32
- // Enable navigation preload if it's supported.
33
- // See https://developers.google.com/web/updates/2017/02/navigation-preload
34
- if ('navigationPreload' in self.registration) {
35
- await self.registration.navigationPreload.enable();
36
- }
37
- })(),
38
- );
39
- // Tell the active service worker to take control of the page immediately.
40
- self.clients.claim();
41
- });
42
-
43
- self.addEventListener('fetch', (event) => {
44
- // Cache-First Strategy
45
- event.respondWith(
46
- (async () => {
47
- // First, try to use the navigation preload response if it's supported.
48
- try {
49
- const preloadResponse = await event.preloadResponse;
50
- if (preloadResponse) return preloadResponse;
51
- return await fetch(event.request);
52
- } catch (error) {
53
- console.error('Fetch failed; returning offline page instead.', event.request.url, error);
54
- // Fallback to the offline page.
55
- const path = PRE_CACHED_RESOURCES.find((path) => event.request.url.match(path.replaceAll('/index.html', '')));
1
+ /**
2
+ * This module provides a configurable Progressive Web App (PWA) service worker and caching strategies.
3
+ * It supports precaching assets, runtime caching with stale-while-revalidate strategy,
4
+ * and offline fallback handling.
5
+ * @module src/client/sw/default.sw.js
6
+ * @namespace PwaServiceWorker
7
+ */
8
+
9
+ /**
10
+ * Class representing a Progressive Web App (PWA) Service Worker with caching strategies.
11
+ * @class
12
+ * @memberof PwaServiceWorker
13
+ */
14
+ class PwaServiceWorker {
15
+ /**
16
+ * Initializes the service worker configuration by reading from self.renderPayload.
17
+ * If properties are not found, defaults are used.
18
+ * @constructor
19
+ * @property {Array<string>} PRE_CACHED_RESOURCES - List of resources to precache.
20
+ * @property {string} CACHE_NAME - Name of the cache storage.
21
+ * @property {string} PROXY_PATH - Base path for proxying requests.
22
+ */
23
+ constructor() {
24
+ // Configuration properties equivalent to the original global constants
25
+ this.PRE_CACHED_RESOURCES = self.renderPayload?.PRE_CACHED_RESOURCES ?? [];
26
+ this.CACHE_NAME = self.renderPayload?.CACHE_NAME ?? 'app-cache';
27
+ this.PROXY_PATH = self.renderPayload?.PROXY_PATH ?? '/';
28
+
29
+ console.log(`Service Worker Initialized. Cache: ${this.CACHE_NAME}, Proxy: ${this.PROXY_PATH}`);
30
+ }
56
31
 
32
+ /**
33
+ * Registers event listeners for the service worker lifecycle and requests.
34
+ * @method
35
+ * @memberof PwaServiceWorker
36
+ */
37
+ run() {
38
+ // Bind methods to 'this' (the instance) before attaching to self
39
+ self.addEventListener('install', this._onInstall.bind(this));
40
+ self.addEventListener('activate', this._onActivate.bind(this));
41
+ self.addEventListener('fetch', this._onFetch.bind(this));
42
+ }
43
+
44
+ /**
45
+ * Handles the 'install' event. Skips waiting and precaches static assets.
46
+ * @param {ExtendableEvent} event
47
+ * @memberof PwaServiceWorker
48
+ */
49
+ _onInstall(event) {
50
+ // Activate right away
51
+ self.skipWaiting();
52
+
53
+ event.waitUntil(
54
+ (async () => {
55
+ // Open the app's cache using the configured name.
56
+ const cache = await caches.open(this.CACHE_NAME);
57
+ // Cache all static resources.
57
58
  try {
58
- const cachedResponse = await caches.match(event.request);
59
- if (cachedResponse) return cachedResponse;
60
- const cache = await caches.open(CACHE_NAME);
61
- const preCachedResponse = await cache.match(path);
62
- if (!preCachedResponse) throw new Error(error.message);
63
- return preCachedResponse;
59
+ console.log(`Precaching ${this.PRE_CACHED_RESOURCES.length} resources...`);
60
+ await cache.addAll(this.PRE_CACHED_RESOURCES);
64
61
  } catch (error) {
65
- console.error('Error opening cache for pre cached page', {
66
- url: event.request.url,
67
- error,
68
- onLine: navigator.onLine,
69
- });
70
- try {
71
- if (!navigator.onLine) {
72
- if (event.request.method.toUpperCase() === 'GET') {
73
- const cache = await caches.open(CACHE_NAME);
74
- const preCachedResponse = await cache.match(
75
- `${PROXY_PATH === '/' ? '' : PROXY_PATH}/offline/index.html`,
76
- );
77
- if (!preCachedResponse) throw new Error(error.message);
78
- return preCachedResponse;
79
- }
80
- const response = new Response(JSON.stringify({ status: 'error', message: 'offline test response' }));
81
- // response.status = 200;
82
- response.headers.set('Content-Type', 'application/json');
83
- return response;
84
- }
62
+ console.error('Error during precaching resources:', error);
63
+ }
64
+ })(),
65
+ );
66
+ }
67
+
68
+ /**
69
+ * Handles the 'activate' event. Enables navigation preload and takes control
70
+ * of uncontrolled clients immediately.
71
+ * @param {ExtendableEvent} event
72
+ * @memberof PwaServiceWorker
73
+ */
74
+ _onActivate(event) {
75
+ event.waitUntil(
76
+ (async () => {
77
+ // Enable navigation preload if it's supported.
78
+ if ('navigationPreload' in self.registration) {
79
+ await self.registration.navigationPreload.enable();
80
+ console.log('Navigation Preload enabled.');
81
+ }
82
+ })(),
83
+ );
84
+ // Tell the active service worker to take control of the page immediately.
85
+ self.clients.claim();
86
+ }
87
+
88
+ /**
89
+ * Handles the 'fetch' event, implementing the Cache-First strategy with
90
+ * complex offline and maintenance fallbacks.
91
+ * @param {FetchEvent} event
92
+ * @memberof PwaServiceWorker
93
+ */
94
+ _onFetch(event) {
95
+ // Only handle HTTP/HTTPS requests that are not cross-origin (optional, but robust)
96
+ if (event.request.url.startsWith('http')) {
97
+ event.respondWith(this._handleFetchRequest(event));
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Core logic to handle fetching, caching, and fallbacks.
103
+ * @param {FetchEvent} event
104
+ * @returns {Promise<Response>}
105
+ * @memberof PwaServiceWorker
106
+ */
107
+ async _handleFetchRequest(event) {
108
+ // 1. Try Navigation Preload (if available) or network first
109
+ try {
110
+ const preloadResponse = await event.preloadResponse;
111
+ if (preloadResponse) return preloadResponse;
112
+
113
+ // Fall through to network request if no preload response
114
+ const networkResponse = await fetch(event.request);
115
+
116
+ // OPTIONAL: If the network request is successful, cache it for future use (stale-while-revalidate logic)
117
+ // Omitted for strict equivalence, as original only had complex fallback, not runtime caching.
118
+
119
+ return networkResponse;
120
+ } catch (error) {
121
+ console.error('Network request failed. Attempting cache/fallback logic.', event.request.url, error);
122
+
123
+ // 2. Try to match the request in the cache
124
+ try {
125
+ const cachedResponse = await caches.match(event.request);
126
+ if (cachedResponse) {
127
+ console.log(`Cache hit for: ${event.request.url}`);
128
+ return cachedResponse;
129
+ }
130
+
131
+ // 3. Try to match a precached resource path (e.g., if requesting /page, match /page/index.html)
132
+ const path = this.PRE_CACHED_RESOURCES.find((p) => event.request.url.match(p.replaceAll('/index.html', '')));
133
+
134
+ if (path) {
135
+ const cache = await caches.open(this.CACHE_NAME);
136
+ const preCachedResponse = await cache.match(path);
137
+ if (preCachedResponse) {
138
+ console.log(`Matched precached resource for: ${event.request.url} via path: ${path}`);
139
+ return preCachedResponse;
140
+ }
141
+ }
142
+
143
+ // If neither cache match nor precache path match worked, fall through to complex fallback
144
+ throw new Error('Cache miss and no precache match.');
145
+ } catch (cacheError) {
146
+ console.error('Error in primary cache lookup. Falling back to offline/maintenance pages.', {
147
+ url: event.request.url,
148
+ cacheError,
149
+ onLine: navigator.onLine,
150
+ });
151
+
152
+ // 4. Complex Fallback Logic (Offline or Maintenance)
153
+ try {
154
+ const cache = await caches.open(this.CACHE_NAME);
155
+
156
+ if (!navigator.onLine) {
157
+ // A. OFFLINE FALLBACK
85
158
  if (event.request.method.toUpperCase() === 'GET') {
86
- const cache = await caches.open(CACHE_NAME);
87
- const preCachedResponse = await cache.match(
88
- `${PROXY_PATH === '/' ? '' : PROXY_PATH}/maintenance/index.html`,
89
- );
90
- if (!preCachedResponse) throw new Error(error.message);
159
+ const offlinePath = `${this.PROXY_PATH === '/' ? '' : this.PROXY_PATH}/offline/index.html`;
160
+ const preCachedResponse = await cache.match(offlinePath);
161
+
162
+ if (!preCachedResponse) throw new Error(`Offline page not found in cache: ${offlinePath}`);
163
+
164
+ console.log('Serving offline HTML page.');
91
165
  return preCachedResponse;
92
166
  }
93
- const response = new Response(JSON.stringify({ status: 'error', message: 'server in maintenance' }));
94
- // response.status = 200;
95
- response.headers.set('Content-Type', 'application/json');
96
- return response;
97
- } catch (error) {
98
- console.error('Error opening cache for offline page', event.request.url, error);
99
- const response = new Response(JSON.stringify({ status: 'error', message: error.message }));
100
- // response.status = 200;
167
+
168
+ // B. OFFLINE API FALLBACK (Non-GET requests)
169
+ console.log('Serving offline JSON response for non-GET request.');
170
+ const response = new Response(JSON.stringify({ status: 'error', message: 'offline test response' }));
101
171
  response.headers.set('Content-Type', 'application/json');
102
172
  return response;
103
173
  }
174
+
175
+ // C. MAINTENANCE FALLBACK (Online, but network failed - interpreted as maintenance)
176
+ if (event.request.method.toUpperCase() === 'GET') {
177
+ const maintenancePath = `${this.PROXY_PATH === '/' ? '' : this.PROXY_PATH}/maintenance/index.html`;
178
+ const preCachedResponse = await cache.match(maintenancePath);
179
+
180
+ if (!preCachedResponse) throw new Error(`Maintenance page not found in cache: ${maintenancePath}`);
181
+
182
+ console.log('Serving maintenance HTML page.');
183
+ return preCachedResponse;
184
+ }
185
+
186
+ // D. MAINTENANCE API FALLBACK (Non-GET requests)
187
+ console.log('Serving maintenance JSON response for non-GET request.');
188
+ const response = new Response(JSON.stringify({ status: 'error', message: 'server in maintenance' }));
189
+ response.headers.set('Content-Type', 'application/json');
190
+ return response;
191
+ } catch (finalError) {
192
+ // 5. Final fail-safe response
193
+ console.error('Final fail-safe execution failed.', event.request.url, finalError);
194
+ const response = new Response(JSON.stringify({ status: 'error', message: finalError.message }));
195
+ response.headers.set('Content-Type', 'application/json');
196
+ return response;
104
197
  }
105
198
  }
106
- })(),
107
- );
108
- });
199
+ }
200
+ }
201
+ }
202
+
203
+ // Instantiate and run the service worker class
204
+ new PwaServiceWorker().run();
package/src/client.dev.js CHANGED
@@ -15,7 +15,7 @@ const logger = loggerFactory(import.meta);
15
15
 
16
16
  await logger.setUpInfo();
17
17
 
18
- await buildClientStaticConf();
18
+ await buildClientStaticConf({ devProxy: process.argv[6] === 'proxy' });
19
19
 
20
20
  await Config.build();
21
21
 
@@ -42,9 +42,9 @@ class MariaDBService {
42
42
  try {
43
43
  conn = await pool.getConnection();
44
44
  result = await conn.query(query, { supportBigNumbers: true, bigNumberStrings: true });
45
- logger.info(query, result);
45
+ logger.info('query');
46
+ console.log(result);
46
47
  } catch (error) {
47
- if (error.stack.startsWith('TypeError: Do not know how to serialize a BigInt')) return;
48
48
  logger.error(error, error.stack);
49
49
  } finally {
50
50
  if (conn) conn.release(); // release to pool
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ class Underpost {
35
35
  * @type {String}
36
36
  * @memberof Underpost
37
37
  */
38
- static version = 'v2.85.1';
38
+ static version = 'v2.89.0';
39
39
  /**
40
40
  * Repository cli API
41
41
  * @static