@zeniai/web-app-ui 5.0.27-dev → 5.0.28-dev

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 (101) hide show
  1. package/dist/assets/AddressRoutes-B2yosjDP.js +1 -0
  2. package/dist/assets/{AddressScreen-Cuxn6jUb.js → AddressScreen-Dli3WBSh.js} +1 -1
  3. package/dist/assets/{AiCfoScreen-DWkNdv1K.js → AiCfoScreen-CNOP96Bi.js} +1 -1
  4. package/dist/assets/{BillPayApprovalRoutes-CLMXdAbQ.js → BillPayApprovalRoutes-DgFkK4Xc.js} +1 -1
  5. package/dist/assets/{BillPayRoutes-BG-syOfw.js → BillPayRoutes-D-pYWAbF.js} +1 -1
  6. package/dist/assets/{BusinessVerificationPageScreen-BB9F88mK.js → BusinessVerificationPageScreen-DKrw7o8b.js} +1 -1
  7. package/dist/assets/{ChargeCardRoutes-DkULQd3q.js → ChargeCardRoutes-B36bJfrv.js} +1 -1
  8. package/dist/assets/{CompanyPassportScreen-DsQqH3R_.js → CompanyPassportScreen-DHSm2BvM.js} +1 -1
  9. package/dist/assets/{ConnectionAuthScreen-UNrq0bOs.js → ConnectionAuthScreen-B4_xqVLG.js} +1 -1
  10. package/dist/assets/CustomerOnboardingAuthScreen-C5RJn3fw.js +1 -0
  11. package/dist/assets/{CustomerOnboardingRoutes-lSSTd4er.js → CustomerOnboardingRoutes-Dez8AewY.js} +1 -1
  12. package/dist/assets/{DashboardRoutes-CITCXfQh.js → DashboardRoutes-CU64wyl_.js} +1 -1
  13. package/dist/assets/{DefaultTenantHome-DnXJprlQ.js → DefaultTenantHome-DEjNA-Qp.js} +1 -1
  14. package/dist/assets/{DomesticWireDetailScreen-DswgjtZ7.js → DomesticWireDetailScreen-DtMAxBfb.js} +1 -1
  15. package/dist/assets/{DrawerScreen-W83sAi_k.js → DrawerScreen-sYvZKN3Y.js} +1 -1
  16. package/dist/assets/{EntityDetailRoutes-DFMGt3cF.js → EntityDetailRoutes-Dwaf84tO.js} +1 -1
  17. package/dist/assets/{ExpenseAutomationRoutes-BwBWvBQz.js → ExpenseAutomationRoutes-guse6js1.js} +1 -1
  18. package/dist/assets/{FeaturePreviewScreen-D_2XoCHl.js → FeaturePreviewScreen--5hLyJ_2.js} +1 -1
  19. package/dist/assets/{MagicLinkRoutes-DMKCfw5O.js → MagicLinkRoutes-lwVnqQEI.js} +1 -1
  20. package/dist/assets/{MagicLinkSignInScreen-BcOqXx3I.js → MagicLinkSignInScreen-B64QZe5w.js} +1 -1
  21. package/dist/assets/{MobileAppDrawer-DvyjBd4g.js → MobileAppDrawer-DI2qJWFS.js} +1 -1
  22. package/dist/assets/NotFoundScreen-CAervBWG.js +1 -0
  23. package/dist/assets/{NotificationRoutes-CgSoQGEX.js → NotificationRoutes-93QACq1j.js} +1 -1
  24. package/dist/assets/{PandLWithForecastRoutes-D6-VHxb-.js → PandLWithForecastRoutes-B5iskeF2.js} +1 -1
  25. package/dist/assets/{PeopleRoutes-Deq8C3bu.js → PeopleRoutes-DVB6InmJ.js} +1 -1
  26. package/dist/assets/{PerformanceRoutes-C8KioDe9.js → PerformanceRoutes-DbX-AREc.js} +1 -1
  27. package/dist/assets/{Preview-BNMITNPE.js → Preview-CcipHtv0.js} +1 -1
  28. package/dist/assets/{QBOConnectionScreen-B-4fO2ne.js → QBOConnectionScreen-mZ7XNaEF.js} +1 -1
  29. package/dist/assets/{ReferralListScreen-BM1gHESu.js → ReferralListScreen-BV4oiI_l.js} +1 -1
  30. package/dist/assets/{ReimbursementApprovalRoutes-Dtwap8JJ.js → ReimbursementApprovalRoutes-RXgckBKl.js} +1 -1
  31. package/dist/assets/{ReimbursementApprovalRuleDetailScreen-B5Ck2C9Y.js → ReimbursementApprovalRuleDetailScreen-DosxwBI7.js} +1 -1
  32. package/dist/assets/{ReimbursementRoutes-cLh7G6_T.js → ReimbursementRoutes-dm5AJ_FK.js} +1 -1
  33. package/dist/assets/{ReportsRoutes-BBHBFqy6.js → ReportsRoutes-B3K6Cpse.js} +1 -1
  34. package/dist/assets/{RewardsRoutes-DBKBcVt9.js → RewardsRoutes-DOVZDpGF.js} +1 -1
  35. package/dist/assets/{ScreenRoutes-zeq48oCM.js → ScreenRoutes-BHXVHS5K.js} +2 -2
  36. package/dist/assets/{SettingsRoutes-Cw1W0wHt.js → SettingsRoutes-m3_cAh3n.js} +1 -1
  37. package/dist/assets/{SetupPagesScreen-C_S8AZRH.js → SetupPagesScreen-BYZmB6RQ.js} +1 -1
  38. package/dist/assets/{SignInScreen-CwqEUOO8.js → SignInScreen-CV3JTfRt.js} +1 -1
  39. package/dist/assets/{SignOutScreen-kZlEM8yn.js → SignOutScreen-CbejG0GQ.js} +1 -1
  40. package/dist/assets/{TaskListScreen-CqTOqB17.js → TaskListScreen-BFiFSKkl.js} +1 -1
  41. package/dist/assets/{TaskRoutes-D4-Lbjxl.js → TaskRoutes-D75AB3w_.js} +1 -1
  42. package/dist/assets/TransactionDetailRoutes-BC1p1DvP.js +1 -0
  43. package/dist/assets/{TransactionDetailScreen-ClRsLpwG.js → TransactionDetailScreen-C43lmhQC.js} +1 -1
  44. package/dist/assets/{TransactionListRoutes-CmhrN_6s.js → TransactionListRoutes-De-RO3Gi.js} +1 -1
  45. package/dist/assets/{TreasuryRoutes-Bgzquerm.js → TreasuryRoutes-W6_vnWoF.js} +1 -1
  46. package/dist/assets/{VendorsRoutes-Df3m6Veu.js → VendorsRoutes-Tvmoq5A8.js} +1 -1
  47. package/dist/assets/{WiseConfirmationScreen-BDSaWGqI.js → WiseConfirmationScreen-BMOK4iFw.js} +1 -1
  48. package/dist/assets/{ZeniAccountRoutes-bTvT8A0g.js → ZeniAccountRoutes-D3OBE_Ss.js} +1 -1
  49. package/dist/assets/{ZeniAccountStatementScreen-COih_FbC.js → ZeniAccountStatementScreen-CF-490Xr.js} +1 -1
  50. package/dist/assets/{accountMappingHelper-Br2QJsWh.js → accountMappingHelper-CZh04DwI.js} +1 -1
  51. package/dist/assets/{analytics-CaofNoIK.js → analytics-B5o5sheK.js} +1 -1
  52. package/dist/assets/{analyticsHelper-26beXk6v.js → analyticsHelper-u-FHaHNW.js} +1 -1
  53. package/dist/assets/{core-r3zPXrnR.js → core-kXknxXmM.js} +1 -1
  54. package/dist/assets/{decodeURIComponentSafe-DIvx_0bc.js → decodeURIComponentSafe-wIBJn5gs.js} +1 -1
  55. package/dist/assets/{dnd-dQAnWf63.js → dnd-D8v1Hu0d.js} +1 -1
  56. package/dist/assets/{empty-DOFWY-gL.js → empty-oIOcDVkl.js} +1 -1
  57. package/dist/assets/{empty-Dguy-xnr.js → empty-wWqNmdcy.js} +1 -1
  58. package/dist/assets/{emptyVideoElement-BqRp7P8T.js → emptyVideoElement-C7vitkya.js} +1 -1
  59. package/dist/assets/{epic-kHR8aIZ1.js → epic-Lmu1wsdA.js} +1 -1
  60. package/dist/assets/{getLocaleForTenant-CQnHebPH.js → getLocaleForTenant-CJft5E8d.js} +1 -1
  61. package/dist/assets/{index-BGpP6oaB.js → index-B9VXLjDS.js} +1 -1
  62. package/dist/assets/{index-DJbMCLsX.js → index-CCGSsCQb.js} +2 -2
  63. package/dist/assets/{index-BTkf3iCj.js → index-CcfqP51k.js} +1 -1
  64. package/dist/assets/{index-DYvO_TO1.js → index-iJlw9gxM.js} +1 -1
  65. package/dist/assets/{index-D-9tzkhM.js → index-qCVVoBzB.js} +1 -1
  66. package/dist/assets/{index-DHwEE904.js → index-r7RBj2Zs.js} +9854 -9870
  67. package/dist/assets/{index-BG4MEhH2.js → index-sfVh4bD9.js} +1 -1
  68. package/dist/assets/{lexical-DPC-NxTc.js → lexical-DTfvcVgU.js} +1 -1
  69. package/dist/assets/{liveblocks-BUJqCtmZ.js → liveblocks-Bl3A2p2B.js} +1 -1
  70. package/dist/assets/{lottie-D8AaXarT.js → lottie-B-MkWC6q.js} +1 -1
  71. package/dist/assets/{mui-BD9ReF4V.js → mui-axh11eoI.js} +1 -1
  72. package/dist/assets/{pathToGoBack-D3dTZRn8.js → pathToGoBack-DfPLhZwj.js} +1 -1
  73. package/dist/assets/{pdf-iZy3i289.js → pdf-VFwJrwPY.js} +3 -3
  74. package/dist/assets/{pdf-lib-C-qCwtZm.js → pdf-lib-B51MNmfW.js} +1 -1
  75. package/dist/assets/{plaid-CxNli5Mg.js → plaid-BUPrZO5b.js} +1 -1
  76. package/dist/assets/{pusher-0ovRh4wm.js → pusher-CAQ8WE0X.js} +1 -1
  77. package/dist/assets/{react-DSIBkdUb.js → react-Be8LqDD0.js} +1 -1
  78. package/dist/assets/{react-DqIQJ9MY.js → react-CFX24kGb.js} +1 -1
  79. package/dist/assets/{react-Bt99sb5m.js → react-CVjxRdIQ.js} +1 -1
  80. package/dist/assets/{react-Db_3nqMy.js → react-DEAMftpa.js} +1 -1
  81. package/dist/assets/{react-Cd-2CZq4.js → react-DbTUBIfc.js} +1 -1
  82. package/dist/assets/{react-DhnULN7m.js → react-xXVp9hUr.js} +1 -1
  83. package/dist/assets/{recharts-DD1DDktN.js → recharts-DUgaG0ww.js} +1 -1
  84. package/dist/assets/{routePaths-BGzy8Dqj.js → routePaths-CuMG5DGQ.js} +1 -1
  85. package/dist/assets/{sentry-BbjYSgze.js → sentry-DDBUQl1l.js} +1 -1
  86. package/dist/assets/{url-BaKOjXZM.js → url-DIsZgeBv.js} +1 -1
  87. package/dist/assets/{url-DYDNdwVU.js → url-iKcR75LE.js} +1 -1
  88. package/dist/assets/{useAskAiCfoHostNavButtonProps-CIdYxL8V.js → useAskAiCfoHostNavButtonProps-BZNd8NRL.js} +1 -1
  89. package/dist/assets/{useDeviceId-DZSzfiw7.js → useDeviceId-BdkLfCQj.js} +1 -1
  90. package/dist/assets/{useFetchSuggestedQuestionsWhenReady-DaXgh6Pr.js → useFetchSuggestedQuestionsWhenReady-DiJocIaN.js} +1 -1
  91. package/dist/assets/{useInitialThreadRequest-C6Fi1v7Q.js → useInitialThreadRequest-DDmfCFY0.js} +1 -1
  92. package/dist/assets/{utils-gH_xiFg7.js → utils-D61OcZ2U.js} +1 -1
  93. package/dist/assets/{withTransactionSidePanel-C7832Ype.js → withTransactionSidePanel-Rd6FsTPQ.js} +1 -1
  94. package/dist/assets/{zeni-epic-state-Cdw8EpcM.js → zeni-epic-state-DMCSky0I.js} +3 -3
  95. package/dist/index.html +1 -1
  96. package/dist/service-worker.js +88 -896
  97. package/package.json +2 -2
  98. package/dist/assets/AddressRoutes-0TGuT2xq.js +0 -1
  99. package/dist/assets/CustomerOnboardingAuthScreen-Cv8y1n9u.js +0 -1
  100. package/dist/assets/NotFoundScreen-BHPCFLTM.js +0 -1
  101. package/dist/assets/TransactionDetailRoutes-BXbmHxwN.js +0 -1
@@ -1,162 +1,130 @@
1
1
  // service-worker.js
2
- // v1.0.2 - Cache-bust helpers so SW + helpers always match after deploy
3
- //
4
- // Deployment: When changing this file or service-worker-helpers.js:
5
- // 1. Bump CACHE_VERSION below so old caches are dropped on activate.
6
- // 2. Deploy both service-worker.js and service-worker-helpers.js together.
7
- // 3. Helpers are loaded with the same query string as the SW (see importScripts
8
- // below), so they are not served from cache after a new registration.
9
- // 4. Clients register with a cache-busting param; skipWaiting() + update flow
10
- // ensure the new worker activates and users get the update without manual steps.
11
- //
12
- // Import helper functions (use same query as SW URL so helpers are not served from cache)
13
- importScripts("service-worker-helpers.js" + self.location.search);
14
2
 
15
3
  // Get environment variable from URL parameters
16
4
  const params = new URLSearchParams(self.location.search);
17
5
  const env = params.get("env");
18
6
  const endpointUrl = params.get("endpoint");
19
7
 
20
- // Cache versioning for clean cache management (bump on every SW/helpers deploy)
21
- const CACHE_VERSION = "v1.0.2"; // Increment this to force cache refresh
22
- const CACHE_NAME = `app-cache-${CACHE_VERSION}-${self.registration.scope}`;
23
- const API_CACHE_NAME = "api-prefetch-cache";
24
- const METADATA_CACHE_NAME = "api-cache-metadata";
8
+ // Cache versioning for clean cache management
9
+ const CACHE_NAME = `app-cache-${self.registration.scope}`;
25
10
 
26
11
  // Set polling interval (30 minutes for PROD, 10 minutes for other environments)
27
12
  const POLLING_INTERVAL =
28
13
  env === "PROD" || env === "NEXT" ? 0.5 * 60 * 60 * 1000 : 10 * 60 * 1000;
29
14
 
30
- // API Cache expiry duration (20 minutes default, configurable from app)
31
- let apiCacheDuration = 1000 * 60 * 20; // milliseconds
32
-
33
- // Cache size limits for memory management
34
- const MAX_CACHE_ENTRIES = 100; // Maximum number of cached URLs
35
- const MAX_CACHE_SIZE_MB = 50; // Maximum cache size in MB
36
- const LOW_STORAGE_THRESHOLD_MB = 100; // Alert if available storage < 100MB
37
- const CACHE_CLEANUP_INTERVAL = 1000 * 60 * 15; // Cleanup every 15 minutes
38
-
39
- // Cache eviction configuration (configurable percentages)
40
- const CACHE_SIZE_EXCEEDED_EVICTION_PERCENT = 0.2; // Evict 20% of entries when cache size limit exceeded
41
- const LOW_STORAGE_EVICTION_PERCENT = 0.5; // Evict 50% of entries when storage is critically low
42
-
43
- // Continuous prefetch configuration
44
- const PREFETCH_INTERVAL = 1000 * 60 * 20; // Re-prefetch every 20 minutes
45
- let prefetchIntervalId = null; // Store interval ID for cleanup
46
- let currentPrefetchJob = null; // Store current prefetch configuration
47
-
48
- // Interval tracking to prevent duplicates on re-activation
49
- let updateCheckIntervalId = null;
50
- let cacheMaintenanceIntervalId = null;
51
-
52
15
  // Incrased the popup hiding interval for PROD env after discussion with product team
53
16
  // Set auto hide interval (96 hours for PROD, 48 hours for other environments)
54
17
  const HIDING_INTERVAL =
55
18
  env === "PROD" || env === "NEXT" ? 96 * 60 * 60 * 1000 : 48 * 60 * 60 * 1000;
56
19
 
57
- // State object for update tracking (passed to helpers)
58
- const updateState = {
59
- lastDeployedTime: null, // Track the last deployment time
60
- updatePending: false, // Flag to track if new assets are available
61
- };
62
-
63
- // Whitelist of URLs that should be prefetched and cached
64
- // Key: normalized URL, Value: tenant_id (to support tenant-specific caching)
65
- const prefetchableURLs = new Map();
66
-
67
- // Track cache access times for LRU eviction
68
- const cacheAccessTimes = new Map(); // Key: cacheKey, Value: timestamp
69
-
70
- // Track page refresh state to bypass cache during refresh
71
- // Key: clientId (string), Value: timestamp when refresh was detected
72
- // Cache is bypassed for requests within REFRESH_BYPASS_WINDOW_MS of refresh
73
- const refreshTimestamps = new Map();
74
- const REFRESH_BYPASS_WINDOW_MS = 10000; // 10 seconds - bypass cache for 10 seconds after refresh
75
-
76
- // Cleanup expired refresh timestamps periodically
77
- setInterval(() => {
78
- const now = Date.now();
79
- for (const [clientId, timestamp] of refreshTimestamps.entries()) {
80
- if (now - timestamp > REFRESH_BYPASS_WINDOW_MS) {
81
- refreshTimestamps.delete(clientId);
82
- }
83
- }
84
- }, 5000); // Clean up every 5 seconds
85
-
86
- // Initialize logging helpers first (needed by other functions)
87
- const {envLog, envWarn, envError} = createLoggingHelpers(env);
88
-
89
- // Initialize update helpers (after logging helpers are available)
90
- const {checkForUpdate, notifyClientsForUpdate, hideUpdatePopupsFromAllClients} =
91
- createUpdateHelpers({
92
- env,
93
- endpointUrl,
94
- HIDING_INTERVAL,
95
- state: updateState,
96
- envError,
97
- });
20
+ let lastDeployedTime = null; // Track the last deployment time
21
+ let updatePending = false; // Flag to track if new assets are available
98
22
 
99
23
  // Install event - activate new SW immediately
100
24
  self.addEventListener("install", (event) => {
101
- envLog(`[Service Worker] 🔧 Installing version ${CACHE_VERSION}`);
102
25
  event.waitUntil(self.skipWaiting()); // Activate the new service worker immediately
103
26
  });
104
27
 
105
28
  // Activate event - claim clients and clear old caches
106
29
  self.addEventListener("activate", (event) => {
107
- envLog(`[Service Worker] ✅ Activating version ${CACHE_VERSION}`);
108
30
  event.waitUntil(
109
31
  (async () => {
110
32
  // Clear old caches during activation
111
33
  const cacheNames = await caches.keys();
112
- envLog(
113
- `[Service Worker] 🗑️ Found ${cacheNames.length} caches, clearing old ones...`
114
- );
115
34
  await Promise.all(
116
- cacheNames.map((cacheName) => {
117
- // Keep current version cache, API cache, and metadata cache
118
- if (
119
- cacheName !== CACHE_NAME &&
120
- cacheName !== API_CACHE_NAME &&
121
- cacheName !== METADATA_CACHE_NAME
122
- ) {
123
- envLog(`[Service Worker] 🗑️ Deleting old cache: ${cacheName}`);
124
- return caches.delete(cacheName);
125
- }
126
- return Promise.resolve();
127
- })
35
+ cacheNames.map((cacheName) =>
36
+ cacheName !== CACHE_NAME
37
+ ? caches.delete(cacheName)
38
+ : Promise.resolve()
39
+ )
128
40
  );
129
41
 
130
- // Clear any existing intervals to prevent duplicates on re-activation
131
- if (updateCheckIntervalId != null) {
132
- clearInterval(updateCheckIntervalId);
133
- updateCheckIntervalId = null;
42
+ // Register periodic sync if supported, fallback to polling otherwise
43
+ try {
44
+ if ("periodicSync" in self.registration) {
45
+ await self.registration.periodicSync.register("check-for-update", {
46
+ minInterval: POLLING_INTERVAL,
47
+ });
48
+ } else {
49
+ setInterval(checkForUpdate, POLLING_INTERVAL);
50
+ }
51
+ } catch (error) {
52
+ console.warn("Periodic sync permission denied:", error);
53
+ setInterval(checkForUpdate, POLLING_INTERVAL);
134
54
  }
135
- if (cacheMaintenanceIntervalId != null) {
136
- clearInterval(cacheMaintenanceIntervalId);
137
- cacheMaintenanceIntervalId = null;
55
+
56
+ await self.clients.claim(); // Take control of open tabs
57
+ })()
58
+ );
59
+ });
60
+
61
+ // Polling or Push-based update check
62
+ async function checkForUpdate() {
63
+ try {
64
+ const response = await fetch(
65
+ `${endpointUrl}/api/last-deployed?env=${env}`,
66
+ {
67
+ cache: "no-cache",
138
68
  }
69
+ );
139
70
 
140
- // Set up polling for update checks (periodicSync requires PWA installation and permissions)
141
- updateCheckIntervalId = setInterval(checkForUpdate, POLLING_INTERVAL);
142
- envLog(
143
- `[Service Worker] ⏰ Update check interval set (${POLLING_INTERVAL / 1000 / 60} minutes)`
144
- );
71
+ if (response.ok) {
72
+ const {lastDeployed} = await response.json();
145
73
 
146
- await self.clients.claim(); // Take control of open tabs
147
- envLog(`[Service Worker] 🎉 Version ${CACHE_VERSION} is now active!`);
74
+ if (lastDeployedTime != null && lastDeployed !== lastDeployedTime) {
75
+ lastDeployedTime = lastDeployed;
76
+ updatePending = true;
148
77
 
149
- // Start periodic cache maintenance
150
- cacheMaintenanceIntervalId = setInterval(
151
- performCacheMaintenance,
152
- CACHE_CLEANUP_INTERVAL
78
+ notifyClientsForUpdate(); // Notify clients about the update
79
+ scheduleBackgroundActivation(); // Schedule background activation
80
+ } else {
81
+ lastDeployedTime = lastDeployed;
82
+ }
83
+ }
84
+ } catch (error) {
85
+ console.error("Error during update check:", error);
86
+ }
87
+ }
88
+
89
+ // Notify clients about the available update
90
+ function notifyClientsForUpdate() {
91
+ self.clients
92
+ .matchAll({type: "window", includeUncontrolled: true})
93
+ .then((clients) => {
94
+ clients.forEach((client) => client.postMessage({type: "UPDATE_READY"}));
95
+ });
96
+ }
97
+
98
+ // Hide available update popups from clients
99
+ function hideUpdatePopupsFromAllClients() {
100
+ self.clients
101
+ .matchAll({type: "window", includeUncontrolled: true})
102
+ .then((clients) => {
103
+ clients.forEach((client) =>
104
+ client.postMessage({type: "HIDE_UPDATE_POPUP"})
153
105
  );
106
+ });
107
+ }
154
108
 
155
- // Perform initial maintenance
156
- await performCacheMaintenance();
157
- })()
158
- );
159
- });
109
+ // Schedule background activation after HIDING_INTERVAL if the user doesn’t act
110
+ function scheduleBackgroundActivation() {
111
+ setTimeout(async () => {
112
+ if (updatePending) {
113
+ // Send a message to the client to hide the popup
114
+ const allClients = await self.clients.matchAll({
115
+ type: "window",
116
+ includeUncontrolled: true,
117
+ });
118
+ allClients.forEach((client) =>
119
+ client.postMessage({type: "HIDE_UPDATE_POPUP"})
120
+ );
121
+
122
+ // Activate the new service worker
123
+ await self.skipWaiting();
124
+ updatePending = false;
125
+ }
126
+ }, HIDING_INTERVAL); // hiding timeout
127
+ }
160
128
 
161
129
  // Handle periodic sync events if supported
162
130
  self.addEventListener("periodicsync", (event) => {
@@ -190,779 +158,3 @@ self.addEventListener("push", (event) => {
190
158
  hideUpdatePopupsFromAllClients();
191
159
  }
192
160
  });
193
-
194
- // Handle messages from the client app
195
- self.addEventListener("message", async (event) => {
196
- // Security: Verify the message origin matches our app's origin
197
- if (event.origin !== self.origin) {
198
- envWarn(
199
- "[Service Worker] ⚠️ Rejected message from untrusted origin:",
200
- event.origin
201
- );
202
- return;
203
- }
204
-
205
- const {type, payload} = event.data ?? {};
206
-
207
- if (type === "DASHBOARD_LOADED") {
208
- envLog(
209
- "[Service Worker] 🎉 Dashboard page loaded! Signal received from Dashboard in Service Worker",
210
- payload
211
- );
212
- }
213
-
214
- if (type === "PREFETCH_URLS") {
215
- envLog("[Service Worker] 📦 Received prefetch URL list");
216
- const {urls, authHeaders, batchSize = 0, expirationTime} = payload;
217
- const tenantId = authHeaders["zeni-tenant-id"];
218
-
219
- if (expirationTime != null) {
220
- envLog(
221
- `[Service Worker] 🎯 Setting expiration time to ${expirationTime} minutes`
222
- );
223
- apiCacheDuration = expirationTime * 1000 * 60; // convert minutes to milliseconds
224
- }
225
-
226
- if (tenantId == null || tenantId === "") {
227
- envError("[Service Worker] ❌ No tenant ID found in auth headers!");
228
- return;
229
- }
230
-
231
- if (urls == null || urls.length === 0) {
232
- envLog("[Service Worker] ⚠️ No URLs to prefetch");
233
- return;
234
- }
235
-
236
- envLog(
237
- `[Service Worker] 🎯 Setting up continuous prefetch for ${urls.length} URLs (tenant: ${tenantId})${batchSize > 0 ? ` with batch size ${batchSize}` : ""}, expiration: ${Math.round(apiCacheDuration / 1000 / 60)}m`
238
- );
239
-
240
- // Add URLs to the prefetchable whitelist with tenant ID (normalized for consistent matching)
241
- urls.forEach((item) => {
242
- const normalizedUrl = normalizeUrl(item.url);
243
- prefetchableURLs.set(normalizedUrl, tenantId);
244
- });
245
- envLog(
246
- `[Service Worker] 📝 Updated prefetch whitelist (${prefetchableURLs.size} total URLs)`
247
- );
248
-
249
- // Start continuous prefetching with interval
250
- startContinuousPrefetch(urls, authHeaders, tenantId, batchSize);
251
- }
252
-
253
- if (type === "STOP_PREFETCH") {
254
- envLog("[Service Worker] 🛑 Received stop prefetch signal");
255
- stopContinuousPrefetch();
256
- }
257
-
258
- if (type === "WAKE_UP") {
259
- envLog("[Service Worker] 👋 Waking up - user returned to tab");
260
- // Service worker is now active and ready to handle requests
261
- }
262
-
263
- if (type === "PAGE_REFRESH") {
264
- // Get client ID from the message source
265
- // event.source can be a Client or MessagePort, we need the client ID
266
- const timestamp = payload?.timestamp ?? Date.now();
267
-
268
- // Try to get client ID from the source
269
- let clientId = null;
270
- if (event.source) {
271
- // If source is a Client, use its id
272
- if ("id" in event.source) {
273
- clientId = event.source.id;
274
- } else if (typeof event.source === "string") {
275
- clientId = event.source;
276
- }
277
- }
278
-
279
- // If we couldn't get client ID from source, try to get it from all clients
280
- // by matching the message origin/timestamp
281
- if (clientId == null || clientId === "") {
282
- try {
283
- const clients = await self.clients.matchAll({
284
- includeUncontrolled: true,
285
- type: "window",
286
- });
287
- // Use the first client as fallback (in most cases there's only one)
288
- if (clients.length > 0) {
289
- clientId = clients[0].id;
290
- }
291
- } catch (error) {
292
- envWarn(
293
- "[Service Worker] Could not get client ID for refresh tracking:",
294
- error
295
- );
296
- }
297
- }
298
-
299
- if (clientId != null && clientId !== "") {
300
- envLog(
301
- `[Service Worker] 🔄 Page refresh detected from client ${clientId} - will bypass cache for ${REFRESH_BYPASS_WINDOW_MS / 1000}s`
302
- );
303
-
304
- // Store refresh timestamp for this client
305
- refreshTimestamps.set(clientId, timestamp);
306
- } else {
307
- // Fallback: use a global refresh flag (less precise but works)
308
- envLog(
309
- `[Service Worker] 🔄 Page refresh detected (client ID unknown) - will bypass cache for ${REFRESH_BYPASS_WINDOW_MS / 1000}s`
310
- );
311
- refreshTimestamps.set("global", timestamp);
312
- }
313
- }
314
- });
315
-
316
- /**
317
- * Generate tenant-specific cache key
318
- */
319
- function getCacheKey(url, tenantId) {
320
- return `${tenantId}:${url}`;
321
- }
322
-
323
- /**
324
- * Store cache metadata (timestamp) for a URL with tenant context
325
- */
326
- async function setCacheMetadata(url, tenantId) {
327
- try {
328
- const cacheKey = getCacheKey(url, tenantId);
329
- const metadataCache = await caches.open(METADATA_CACHE_NAME);
330
- const metadata = {
331
- url: url,
332
- tenantId: tenantId,
333
- cachedAt: Date.now(),
334
- expiresAt: Date.now() + apiCacheDuration,
335
- };
336
-
337
- // Store metadata as a Response object with tenant-specific key
338
- const metadataResponse = new Response(JSON.stringify(metadata), {
339
- headers: {"Content-Type": "application/json"},
340
- });
341
-
342
- await metadataCache.put(cacheKey, metadataResponse);
343
- } catch (error) {
344
- envError("[Service Worker] Error storing cache metadata:", error);
345
- }
346
- }
347
-
348
- /**
349
- * Get cache metadata for a URL with tenant context
350
- */
351
- async function getCacheMetadata(url, tenantId) {
352
- try {
353
- const cacheKey = getCacheKey(url, tenantId);
354
- const metadataCache = await caches.open(METADATA_CACHE_NAME);
355
- const metadataResponse = await metadataCache.match(cacheKey);
356
-
357
- if (metadataResponse) {
358
- const metadata = await metadataResponse.json();
359
- return metadata;
360
- }
361
- return null;
362
- } catch (error) {
363
- envError("[Service Worker] Error reading cache metadata:", error);
364
- return null;
365
- }
366
- }
367
-
368
- /**
369
- * Check if cached data is still valid (not expired) for specific tenant
370
- */
371
- async function isCacheValid(url, tenantId) {
372
- const metadata = await getCacheMetadata(url, tenantId);
373
-
374
- if (metadata == null) {
375
- return false;
376
- }
377
-
378
- const now = Date.now();
379
- const isValid = now < metadata.expiresAt;
380
-
381
- if (!isValid) {
382
- const ageMinutes = Math.round((now - metadata.cachedAt) / 1000 / 60);
383
- envLog(
384
- `[Service Worker] ⏰ Cache expired for tenant ${tenantId}, URL: ${url} (age: ${ageMinutes} minutes)`
385
- );
386
- }
387
-
388
- return isValid;
389
- }
390
-
391
- // Initialize service worker helpers (after isCacheValid is defined)
392
- const {
393
- isCommonAPIUrl,
394
- shouldProcessUrl,
395
- normalizeUrl,
396
- checkStorageQuota,
397
- getCacheEntryCount,
398
- recordCacheAccess,
399
- performCacheMaintenance,
400
- } = createServiceWorkerHelpers({
401
- API_CACHE_NAME,
402
- METADATA_CACHE_NAME,
403
- MAX_CACHE_ENTRIES,
404
- MAX_CACHE_SIZE_MB,
405
- LOW_STORAGE_THRESHOLD_MB,
406
- CACHE_SIZE_EXCEEDED_EVICTION_PERCENT,
407
- LOW_STORAGE_EVICTION_PERCENT,
408
- cacheAccessTimes,
409
- isCacheValid,
410
- envLog,
411
- envWarn,
412
- envError,
413
- URL_PATTERNS_TO_PROCESS: [
414
- "/accounting/2.0/reports/balance_sheet",
415
- "/accounting/2.0/reports/profit_and_loss",
416
- "/accounting/2.0/reports/cash_flow",
417
- ], // Configurable URL patterns
418
- });
419
-
420
- /**
421
- * Delete cache metadata for a URL with tenant context
422
- */
423
- async function deleteCacheMetadata(url, tenantId) {
424
- try {
425
- const cacheKey = getCacheKey(url, tenantId);
426
- const metadataCache = await caches.open(METADATA_CACHE_NAME);
427
- await metadataCache.delete(cacheKey);
428
- } catch (error) {
429
- envError("[Service Worker] Error deleting cache metadata:", error);
430
- }
431
- }
432
-
433
- /**
434
- * Execute prefetch for stored job
435
- * Called immediately and at intervals
436
- * Supports batching - if batchSize > 0, URLs are fetched in batches with each batch waiting for completion
437
- */
438
- async function executePrefetch() {
439
- if (currentPrefetchJob == null) {
440
- envWarn("[Service Worker] ⚠️ No prefetch job to execute");
441
- return;
442
- }
443
-
444
- const {urls, authHeaders, tenantId, batchSize = 0} = currentPrefetchJob;
445
-
446
- envLog(
447
- `[Service Worker] 🔄 Executing periodic prefetch for tenant ${tenantId} (${urls.length} URLs)${batchSize > 0 ? ` in batches of ${batchSize}` : ""}`
448
- );
449
-
450
- // Check storage quota once at the beginning of prefetch cycle
451
- const storageInfo = await checkStorageQuota();
452
- if (storageInfo.low === true) {
453
- envWarn(
454
- `[Service Worker] ⚠️ Low storage detected (${storageInfo.availableMB.toFixed(2)}MB available), performing maintenance before prefetch...`
455
- );
456
- await performCacheMaintenance();
457
- // Re-check storage after maintenance
458
- const updatedStorageInfo = await checkStorageQuota();
459
- if (updatedStorageInfo.low === true) {
460
- envWarn(
461
- `[Service Worker] ⚠️ Storage still low after maintenance, skipping prefetch cycle`
462
- );
463
- return;
464
- }
465
- }
466
-
467
- if (batchSize > 0) {
468
- // Process URLs in batches, waiting for each batch to complete
469
- const totalBatches = Math.ceil(urls.length / batchSize);
470
-
471
- for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
472
- const startIdx = batchIndex * batchSize;
473
- const endIdx = Math.min(startIdx + batchSize, urls.length);
474
- const batch = urls.slice(startIdx, endIdx);
475
-
476
- envLog(
477
- `[Service Worker] 📦 Processing batch ${batchIndex + 1}/${totalBatches} (${batch.length} URLs)`
478
- );
479
-
480
- // Fetch all URLs in this batch concurrently and wait for all to complete
481
- const batchPromises = batch.map((item, indexInBatch) => {
482
- const globalIndex = startIdx + indexInBatch + 1;
483
- return prefetchURL(
484
- item.url,
485
- item.priority,
486
- item.apiType,
487
- globalIndex,
488
- authHeaders,
489
- tenantId,
490
- true // skipStorageCheck - already checked at batch level
491
- );
492
- });
493
-
494
- // Wait for all URLs in this batch to complete before moving to next batch
495
- await Promise.allSettled(batchPromises);
496
-
497
- envLog(
498
- `[Service Worker] ✅ Batch ${batchIndex + 1}/${totalBatches} completed`
499
- );
500
- }
501
-
502
- envLog(`[Service Worker] 🎉 All ${totalBatches} batches completed`);
503
- } else {
504
- // Prefetch all URLs concurrently (original behavior)
505
- urls.forEach((item, index) => {
506
- prefetchURL(
507
- item.url,
508
- item.priority,
509
- item.apiType,
510
- index + 1,
511
- authHeaders,
512
- tenantId,
513
- true // skipStorageCheck - already checked at batch level
514
- );
515
- });
516
- }
517
- }
518
-
519
- /**
520
- * Start continuous prefetching at intervals
521
- * @param {Array} urls - List of URLs to prefetch
522
- * @param {Object} authHeaders - Authentication headers
523
- * @param {string} tenantId - Tenant ID
524
- * @param {number} batchSize - If > 0, URLs will be fetched in batches of this size
525
- */
526
- function startContinuousPrefetch(urls, authHeaders, tenantId, batchSize = 0) {
527
- // Clear any existing interval
528
- stopContinuousPrefetch();
529
-
530
- // Store the current prefetch job with batch size
531
- currentPrefetchJob = {urls, authHeaders, tenantId, batchSize};
532
-
533
- envLog(
534
- `[Service Worker] ⏰ Starting continuous prefetch (interval: ${PREFETCH_INTERVAL / 1000 / 60} minutes)${batchSize > 0 ? ` with batch size ${batchSize}` : ""}`
535
- );
536
-
537
- // Execute immediately
538
- executePrefetch();
539
-
540
- // Set up interval for continuous prefetching
541
- prefetchIntervalId = setInterval(() => {
542
- envLog(`[Service Worker] 🔄 Periodic prefetch triggered`);
543
- executePrefetch();
544
- }, PREFETCH_INTERVAL);
545
- }
546
-
547
- /**
548
- * Stop continuous prefetching
549
- */
550
- function stopContinuousPrefetch() {
551
- if (prefetchIntervalId != null) {
552
- clearInterval(prefetchIntervalId);
553
- prefetchIntervalId = null;
554
- envLog("[Service Worker] ⏹️ Stopped continuous prefetch");
555
- }
556
- currentPrefetchJob = null;
557
- }
558
-
559
- /**
560
- * Prefetch a single URL and cache it with tenant-specific key
561
- * Only fetches if cache is expired or doesn't exist for this tenant
562
- * Skips prefetch for "common" API types (they will be cached on-demand)
563
- * @param {boolean} skipStorageCheck - If true, skips storage quota check (already checked at batch level)
564
- */
565
- async function prefetchURL(
566
- url,
567
- priority,
568
- apiType,
569
- index,
570
- authHeaders,
571
- tenantId,
572
- skipStorageCheck = false
573
- ) {
574
- try {
575
- // Normalize URL for consistent cache key generation
576
- const normalizedUrl = normalizeUrl(url);
577
-
578
- // Check storage quota before prefetching (only if not already checked at batch level)
579
- if (!skipStorageCheck) {
580
- const storageInfo = await checkStorageQuota();
581
- if (storageInfo.low === true) {
582
- envWarn(
583
- `[Service Worker] ⚠️ ${index}. Skipping prefetch due to low storage (${storageInfo.availableMB.toFixed(2)}MB available)`
584
- );
585
- // Trigger maintenance to free up space
586
- await performCacheMaintenance();
587
- return;
588
- }
589
- }
590
-
591
- // Check if URL is already cached and still valid for this tenant
592
- const cache = await caches.open(API_CACHE_NAME);
593
- const cacheKey = getCacheKey(normalizedUrl, tenantId);
594
- const cachedResponse = await cache.match(cacheKey);
595
-
596
- if (cachedResponse) {
597
- const isValid = await isCacheValid(normalizedUrl, tenantId);
598
-
599
- if (isValid) {
600
- // Cache is still valid for this tenant, skip prefetch
601
- const metadata = await getCacheMetadata(normalizedUrl, tenantId);
602
- const ageMinutes = Math.round(
603
- (Date.now() - metadata.cachedAt) / 1000 / 60
604
- );
605
- const remainingMinutes = Math.round(
606
- (metadata.expiresAt - Date.now()) / 1000 / 60
607
- );
608
-
609
- // Record access for LRU tracking
610
- recordCacheAccess(cacheKey);
611
-
612
- envLog(
613
- `[Service Worker] ⏭️ ${index}. Skipping prefetch for tenant ${tenantId} - ${url} (already cached, age: ${ageMinutes}m, expires in: ${remainingMinutes}m)`
614
- );
615
- return; // Skip prefetch
616
- } else {
617
- // Cache expired for this tenant, will refetch
618
- const metadata = await getCacheMetadata(normalizedUrl, tenantId);
619
- const ageMinutes = Math.round(
620
- (Date.now() - metadata.cachedAt) / 1000 / 60
621
- );
622
- envLog(
623
- `[Service Worker] 🔄 ${index}. Cache expired for tenant ${tenantId} (age: ${ageMinutes}m), refetching - ${url}`
624
- );
625
- }
626
- }
627
-
628
- // Check cache limits before fetching
629
- const entryCount = await getCacheEntryCount(API_CACHE_NAME);
630
- if (entryCount >= MAX_CACHE_ENTRIES) {
631
- envWarn(
632
- `[Service Worker] ⚠️ ${index}. Cache limit reached (${entryCount}/${MAX_CACHE_ENTRIES}), performing maintenance...`
633
- );
634
- await performCacheMaintenance();
635
- }
636
-
637
- // Fetch the URL (either not cached or expired for this tenant)
638
- envLog(
639
- `[Service Worker] [${priority}] ${index}. Fetching for tenant ${tenantId}: ${url}`
640
- );
641
- envLog("[Service Worker] 🔑 Auth headers:", authHeaders);
642
- let response;
643
- try {
644
- response = await fetch(url, {
645
- method: "GET",
646
- headers: authHeaders,
647
- });
648
- } catch (error) {
649
- envError(`[Service Worker] ❌ ${index}. Error:`, error.message, url);
650
- return;
651
- }
652
-
653
- if (response.ok) {
654
- // Cache the response with tenant-specific key (using normalized URL)
655
- await cache.put(cacheKey, response.clone());
656
-
657
- // Store cache metadata with timestamp and tenant ID
658
- await setCacheMetadata(normalizedUrl, tenantId);
659
-
660
- // Record access for LRU tracking
661
- recordCacheAccess(cacheKey);
662
-
663
- envLog(
664
- `[Service Worker] ✅ ${index}. Cached successfully for tenant (${apiType}) ${tenantId} (expires in ${apiCacheDuration / 1000 / 60} minutes)`
665
- );
666
- } else {
667
- envWarn(
668
- `[Service Worker] ❌ ${index}. Failed with status: ${response.status}`
669
- );
670
- }
671
- } catch (error) {
672
- envError(`[Service Worker] ❌ ${index}. Error:`, error.message);
673
- }
674
- }
675
-
676
- // Intercept fetch requests to serve cached prefetched data (tenant-aware)
677
- self.addEventListener("fetch", (event) => {
678
- const {request} = event;
679
-
680
- // Check for signout API call - clear all caches
681
- if (
682
- request.url.includes("/auth/1.0/signout") === true &&
683
- request.method === "POST"
684
- ) {
685
- envLog("[Service Worker] 🚪 Signout detected, clearing all caches...");
686
- setTimeout(() => {
687
- clearAllCaches();
688
- }, 1000);
689
- return;
690
- }
691
-
692
- // Check if URL should be processed based on configured patterns
693
- const shouldProcess = shouldProcessUrl(request.url);
694
- if (shouldProcess === true) {
695
- envLog(
696
- `[Service Worker] 🔄 URL should be processed, processing: ${request.url}`
697
- );
698
- } else {
699
- return;
700
- }
701
-
702
- // Only handle GET requests for caching
703
- if (request.method !== "GET") {
704
- return;
705
- }
706
-
707
- // Extract tenant ID from request headers - required for cache lookup
708
- const tenantId = request.headers.get("zeni-tenant-id");
709
-
710
- // Skip if no tenant ID (can't do tenant-specific caching)
711
- if (tenantId == null || tenantId === "") {
712
- return;
713
- }
714
-
715
- // Check if this request is part of a page refresh (should bypass cache)
716
- // We'll check this inside the respondWith handler where we can properly await client lookup
717
-
718
- // Normalize request URL for consistent cache key matching
719
- const normalizedRequestUrl = normalizeUrl(request.url);
720
- const cacheKey = getCacheKey(normalizedRequestUrl, tenantId);
721
-
722
- // Check if this URL is in our in-memory prefetch whitelist
723
- const isInPrefetchWhitelist = prefetchableURLs.has(normalizedRequestUrl);
724
- const whitelistTenantId = prefetchableURLs.get(normalizedRequestUrl);
725
-
726
- // Check if this is a common API URL (always cacheable)
727
- const isCommonUrl = isCommonAPIUrl(normalizedRequestUrl);
728
-
729
- // Determine if URL should be cached (either in whitelist or is common API)
730
- const isCacheable = isInPrefetchWhitelist || isCommonUrl;
731
-
732
- // If in whitelist but different tenant, skip (tenant mismatch)
733
- if (isInPrefetchWhitelist && whitelistTenantId !== tenantId) {
734
- envLog(
735
- `[Service Worker] 🔄 Tenant mismatch for ${request.url} (expected: ${whitelistTenantId}, got: ${tenantId}), passing through`
736
- );
737
- return;
738
- }
739
-
740
- // Respond with cache-first strategy (unless it's a refresh)
741
- // Check cache directly (don't rely solely on in-memory whitelist which clears on SW restart)
742
- event.respondWith(
743
- (async () => {
744
- try {
745
- // Check if this request is part of a page refresh (should bypass cache)
746
- let isRefreshRequest = false;
747
- try {
748
- if (event.clientId != null && event.clientId !== "") {
749
- // Check if there's a recent refresh timestamp for this client
750
- const refreshTimestamp = refreshTimestamps.get(event.clientId);
751
- if (refreshTimestamp != null) {
752
- const now = Date.now();
753
- const timeSinceRefresh = now - refreshTimestamp;
754
-
755
- if (timeSinceRefresh < REFRESH_BYPASS_WINDOW_MS) {
756
- isRefreshRequest = true;
757
- envLog(
758
- `[Service Worker] 🔄 Refresh detected (${Math.round(timeSinceRefresh / 1000)}s ago) - bypassing cache for: ${request.url}`
759
- );
760
- } else {
761
- // Clean up expired refresh timestamp
762
- refreshTimestamps.delete(event.clientId);
763
- }
764
- }
765
-
766
- // Also check global refresh flag as fallback
767
- if (!isRefreshRequest) {
768
- const globalRefreshTimestamp = refreshTimestamps.get("global");
769
- if (globalRefreshTimestamp != null) {
770
- const now = Date.now();
771
- const timeSinceRefresh = now - globalRefreshTimestamp;
772
-
773
- if (timeSinceRefresh < REFRESH_BYPASS_WINDOW_MS) {
774
- isRefreshRequest = true;
775
- envLog(
776
- `[Service Worker] 🔄 Global refresh detected (${Math.round(timeSinceRefresh / 1000)}s ago) - bypassing cache for: ${request.url}`
777
- );
778
- } else {
779
- // Clean up expired global refresh timestamp
780
- refreshTimestamps.delete("global");
781
- }
782
- }
783
- }
784
- }
785
- } catch (error) {
786
- // If we can't determine client, continue normally (not a refresh)
787
- envLog(
788
- "[Service Worker] Could not determine client for refresh check (normal for non-client requests)",
789
- error
790
- );
791
- }
792
-
793
- // If this is a refresh request, bypass cache and fetch fresh data
794
- if (isRefreshRequest) {
795
- envLog(
796
- `[Service Worker] 🔄 Refresh request - bypassing cache and fetching fresh data: ${request.url}`
797
- );
798
-
799
- let networkResponse;
800
- try {
801
- networkResponse = await fetch(request);
802
- } catch (error) {
803
- envError(
804
- "[Service Worker] ❌ Error during fetch:",
805
- error,
806
- request.url
807
- );
808
- return;
809
- }
810
-
811
- // Cache the fresh response if URL is in prefetch whitelist or is a common API URL
812
- if (networkResponse.ok && isCacheable === true) {
813
- const cache = await caches.open(API_CACHE_NAME);
814
- await cache.put(cacheKey, networkResponse.clone());
815
- await setCacheMetadata(normalizedRequestUrl, tenantId);
816
- recordCacheAccess(cacheKey);
817
- envLog(
818
- `[Service Worker] ✅ Fetched and cached fresh data (refresh) for tenant ${tenantId}: ${request.url}${isCommonUrl === true ? " (common API)" : ""}`
819
- );
820
- }
821
-
822
- return networkResponse;
823
- }
824
-
825
- // Normal flow: check cache first
826
- const cache = await caches.open(API_CACHE_NAME);
827
- const cachedResponse = await cache.match(cacheKey);
828
-
829
- // Check if cache exists and is still valid for this tenant
830
- if (cachedResponse) {
831
- const isValid = await isCacheValid(normalizedRequestUrl, tenantId);
832
-
833
- if (isValid) {
834
- // Cache is still valid for this tenant, serve it immediately
835
- const metadata = await getCacheMetadata(
836
- normalizedRequestUrl,
837
- tenantId
838
- );
839
-
840
- const ageMinutes = metadata
841
- ? Math.round((Date.now() - metadata.cachedAt) / 1000 / 60)
842
- : 0;
843
-
844
- // Record access for LRU tracking
845
- recordCacheAccess(cacheKey);
846
-
847
- // Add URL to in-memory whitelist if not already there (restore after SW restart)
848
- // Also add common URLs for tracking
849
- if (!isInPrefetchWhitelist && isCacheable === true) {
850
- prefetchableURLs.set(normalizedRequestUrl, tenantId);
851
- envLog(
852
- `[Service Worker] 📝 Restored ${normalizedRequestUrl} to prefetch whitelist from cache${isCommonUrl === true ? " (common API)" : ""}`
853
- );
854
- }
855
-
856
- envLog(
857
- `[Service Worker] ⚡ Serving from cache for tenant ${tenantId} (age: ${ageMinutes}m): ${request.url}`
858
- );
859
-
860
- return cachedResponse;
861
- } else {
862
- // Cache expired for this tenant, fetch fresh data
863
- envLog(
864
- `[Service Worker] 🔄 Cache expired for tenant ${tenantId}, fetching fresh data: ${request.url}`
865
- );
866
-
867
- // Delete expired cache and metadata
868
- await cache.delete(cacheKey);
869
- await deleteCacheMetadata(normalizedRequestUrl, tenantId);
870
-
871
- let networkResponse;
872
- try {
873
- networkResponse = await fetch(request);
874
- } catch (error) {
875
- envError(
876
- "[Service Worker] ❌ Error during fetch:",
877
- error,
878
- request.url
879
- );
880
- return;
881
- }
882
-
883
- // Cache if URL is in prefetch whitelist or is a common API URL
884
- if (networkResponse.ok && isCacheable === true) {
885
- await cache.put(cacheKey, networkResponse.clone());
886
- await setCacheMetadata(normalizedRequestUrl, tenantId);
887
- recordCacheAccess(cacheKey);
888
- envLog(
889
- `[Service Worker] ✅ Fetched and cached fresh data for tenant ${tenantId}${isCommonUrl === true ? " (common API)" : ""}`
890
- );
891
- }
892
-
893
- return networkResponse;
894
- }
895
- }
896
-
897
- // Not in cache - fetch from network
898
- let networkResponse;
899
- try {
900
- networkResponse = await fetch(request);
901
- } catch (error) {
902
- envError(
903
- "[Service Worker] ❌ Error during fetch:",
904
- error,
905
- request.url
906
- );
907
- return;
908
- }
909
-
910
- // Cache if URL is in prefetch whitelist or is a common API URL
911
- if (networkResponse.ok && isCacheable === true) {
912
- await cache.put(cacheKey, networkResponse.clone());
913
- await setCacheMetadata(normalizedRequestUrl, tenantId);
914
- recordCacheAccess(cacheKey);
915
- envLog(
916
- `[Service Worker] ✅ Fetched and cached for tenant ${tenantId}: ${request.url}${isCommonUrl === true ? " (common API)" : ""}`
917
- );
918
- }
919
-
920
- return networkResponse;
921
- } catch (error) {
922
- envError(
923
- `[Service Worker] ❌ Fetch error for tenant ${tenantId}:`,
924
- error,
925
- request.url
926
- );
927
- return;
928
- }
929
- })()
930
- );
931
- });
932
-
933
- /**
934
- * Clear all caches (API cache and metadata)
935
- * Called on signout to ensure no data persists
936
- */
937
- async function clearAllCaches() {
938
- try {
939
- envLog("[Service Worker] 🧹 Starting cache cleanup...");
940
-
941
- // Stop continuous prefetch
942
- stopContinuousPrefetch();
943
-
944
- // Clear API prefetch cache
945
- const apiCacheDeleted = await caches.delete(API_CACHE_NAME);
946
- if (apiCacheDeleted) {
947
- envLog(`[Service Worker] ✅ Cleared ${API_CACHE_NAME}`);
948
- }
949
-
950
- // Clear metadata cache
951
- const metadataCacheDeleted = await caches.delete(METADATA_CACHE_NAME);
952
- if (metadataCacheDeleted) {
953
- envLog(`[Service Worker] ✅ Cleared ${METADATA_CACHE_NAME}`);
954
- }
955
-
956
- // Clear prefetchable URLs whitelist
957
- prefetchableURLs.clear();
958
- envLog("[Service Worker] ✅ Cleared prefetch whitelist");
959
-
960
- // Clear LRU access times
961
- cacheAccessTimes.clear();
962
- envLog("[Service Worker] ✅ Cleared cache access times");
963
-
964
- envLog("[Service Worker] 🎉 All caches cleared successfully");
965
- } catch (error) {
966
- envError("[Service Worker] ❌ Error clearing caches:", error);
967
- }
968
- }