@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.
- package/dist/assets/AddressRoutes-B2yosjDP.js +1 -0
- package/dist/assets/{AddressScreen-Cuxn6jUb.js → AddressScreen-Dli3WBSh.js} +1 -1
- package/dist/assets/{AiCfoScreen-DWkNdv1K.js → AiCfoScreen-CNOP96Bi.js} +1 -1
- package/dist/assets/{BillPayApprovalRoutes-CLMXdAbQ.js → BillPayApprovalRoutes-DgFkK4Xc.js} +1 -1
- package/dist/assets/{BillPayRoutes-BG-syOfw.js → BillPayRoutes-D-pYWAbF.js} +1 -1
- package/dist/assets/{BusinessVerificationPageScreen-BB9F88mK.js → BusinessVerificationPageScreen-DKrw7o8b.js} +1 -1
- package/dist/assets/{ChargeCardRoutes-DkULQd3q.js → ChargeCardRoutes-B36bJfrv.js} +1 -1
- package/dist/assets/{CompanyPassportScreen-DsQqH3R_.js → CompanyPassportScreen-DHSm2BvM.js} +1 -1
- package/dist/assets/{ConnectionAuthScreen-UNrq0bOs.js → ConnectionAuthScreen-B4_xqVLG.js} +1 -1
- package/dist/assets/CustomerOnboardingAuthScreen-C5RJn3fw.js +1 -0
- package/dist/assets/{CustomerOnboardingRoutes-lSSTd4er.js → CustomerOnboardingRoutes-Dez8AewY.js} +1 -1
- package/dist/assets/{DashboardRoutes-CITCXfQh.js → DashboardRoutes-CU64wyl_.js} +1 -1
- package/dist/assets/{DefaultTenantHome-DnXJprlQ.js → DefaultTenantHome-DEjNA-Qp.js} +1 -1
- package/dist/assets/{DomesticWireDetailScreen-DswgjtZ7.js → DomesticWireDetailScreen-DtMAxBfb.js} +1 -1
- package/dist/assets/{DrawerScreen-W83sAi_k.js → DrawerScreen-sYvZKN3Y.js} +1 -1
- package/dist/assets/{EntityDetailRoutes-DFMGt3cF.js → EntityDetailRoutes-Dwaf84tO.js} +1 -1
- package/dist/assets/{ExpenseAutomationRoutes-BwBWvBQz.js → ExpenseAutomationRoutes-guse6js1.js} +1 -1
- package/dist/assets/{FeaturePreviewScreen-D_2XoCHl.js → FeaturePreviewScreen--5hLyJ_2.js} +1 -1
- package/dist/assets/{MagicLinkRoutes-DMKCfw5O.js → MagicLinkRoutes-lwVnqQEI.js} +1 -1
- package/dist/assets/{MagicLinkSignInScreen-BcOqXx3I.js → MagicLinkSignInScreen-B64QZe5w.js} +1 -1
- package/dist/assets/{MobileAppDrawer-DvyjBd4g.js → MobileAppDrawer-DI2qJWFS.js} +1 -1
- package/dist/assets/NotFoundScreen-CAervBWG.js +1 -0
- package/dist/assets/{NotificationRoutes-CgSoQGEX.js → NotificationRoutes-93QACq1j.js} +1 -1
- package/dist/assets/{PandLWithForecastRoutes-D6-VHxb-.js → PandLWithForecastRoutes-B5iskeF2.js} +1 -1
- package/dist/assets/{PeopleRoutes-Deq8C3bu.js → PeopleRoutes-DVB6InmJ.js} +1 -1
- package/dist/assets/{PerformanceRoutes-C8KioDe9.js → PerformanceRoutes-DbX-AREc.js} +1 -1
- package/dist/assets/{Preview-BNMITNPE.js → Preview-CcipHtv0.js} +1 -1
- package/dist/assets/{QBOConnectionScreen-B-4fO2ne.js → QBOConnectionScreen-mZ7XNaEF.js} +1 -1
- package/dist/assets/{ReferralListScreen-BM1gHESu.js → ReferralListScreen-BV4oiI_l.js} +1 -1
- package/dist/assets/{ReimbursementApprovalRoutes-Dtwap8JJ.js → ReimbursementApprovalRoutes-RXgckBKl.js} +1 -1
- package/dist/assets/{ReimbursementApprovalRuleDetailScreen-B5Ck2C9Y.js → ReimbursementApprovalRuleDetailScreen-DosxwBI7.js} +1 -1
- package/dist/assets/{ReimbursementRoutes-cLh7G6_T.js → ReimbursementRoutes-dm5AJ_FK.js} +1 -1
- package/dist/assets/{ReportsRoutes-BBHBFqy6.js → ReportsRoutes-B3K6Cpse.js} +1 -1
- package/dist/assets/{RewardsRoutes-DBKBcVt9.js → RewardsRoutes-DOVZDpGF.js} +1 -1
- package/dist/assets/{ScreenRoutes-zeq48oCM.js → ScreenRoutes-BHXVHS5K.js} +2 -2
- package/dist/assets/{SettingsRoutes-Cw1W0wHt.js → SettingsRoutes-m3_cAh3n.js} +1 -1
- package/dist/assets/{SetupPagesScreen-C_S8AZRH.js → SetupPagesScreen-BYZmB6RQ.js} +1 -1
- package/dist/assets/{SignInScreen-CwqEUOO8.js → SignInScreen-CV3JTfRt.js} +1 -1
- package/dist/assets/{SignOutScreen-kZlEM8yn.js → SignOutScreen-CbejG0GQ.js} +1 -1
- package/dist/assets/{TaskListScreen-CqTOqB17.js → TaskListScreen-BFiFSKkl.js} +1 -1
- package/dist/assets/{TaskRoutes-D4-Lbjxl.js → TaskRoutes-D75AB3w_.js} +1 -1
- package/dist/assets/TransactionDetailRoutes-BC1p1DvP.js +1 -0
- package/dist/assets/{TransactionDetailScreen-ClRsLpwG.js → TransactionDetailScreen-C43lmhQC.js} +1 -1
- package/dist/assets/{TransactionListRoutes-CmhrN_6s.js → TransactionListRoutes-De-RO3Gi.js} +1 -1
- package/dist/assets/{TreasuryRoutes-Bgzquerm.js → TreasuryRoutes-W6_vnWoF.js} +1 -1
- package/dist/assets/{VendorsRoutes-Df3m6Veu.js → VendorsRoutes-Tvmoq5A8.js} +1 -1
- package/dist/assets/{WiseConfirmationScreen-BDSaWGqI.js → WiseConfirmationScreen-BMOK4iFw.js} +1 -1
- package/dist/assets/{ZeniAccountRoutes-bTvT8A0g.js → ZeniAccountRoutes-D3OBE_Ss.js} +1 -1
- package/dist/assets/{ZeniAccountStatementScreen-COih_FbC.js → ZeniAccountStatementScreen-CF-490Xr.js} +1 -1
- package/dist/assets/{accountMappingHelper-Br2QJsWh.js → accountMappingHelper-CZh04DwI.js} +1 -1
- package/dist/assets/{analytics-CaofNoIK.js → analytics-B5o5sheK.js} +1 -1
- package/dist/assets/{analyticsHelper-26beXk6v.js → analyticsHelper-u-FHaHNW.js} +1 -1
- package/dist/assets/{core-r3zPXrnR.js → core-kXknxXmM.js} +1 -1
- package/dist/assets/{decodeURIComponentSafe-DIvx_0bc.js → decodeURIComponentSafe-wIBJn5gs.js} +1 -1
- package/dist/assets/{dnd-dQAnWf63.js → dnd-D8v1Hu0d.js} +1 -1
- package/dist/assets/{empty-DOFWY-gL.js → empty-oIOcDVkl.js} +1 -1
- package/dist/assets/{empty-Dguy-xnr.js → empty-wWqNmdcy.js} +1 -1
- package/dist/assets/{emptyVideoElement-BqRp7P8T.js → emptyVideoElement-C7vitkya.js} +1 -1
- package/dist/assets/{epic-kHR8aIZ1.js → epic-Lmu1wsdA.js} +1 -1
- package/dist/assets/{getLocaleForTenant-CQnHebPH.js → getLocaleForTenant-CJft5E8d.js} +1 -1
- package/dist/assets/{index-BGpP6oaB.js → index-B9VXLjDS.js} +1 -1
- package/dist/assets/{index-DJbMCLsX.js → index-CCGSsCQb.js} +2 -2
- package/dist/assets/{index-BTkf3iCj.js → index-CcfqP51k.js} +1 -1
- package/dist/assets/{index-DYvO_TO1.js → index-iJlw9gxM.js} +1 -1
- package/dist/assets/{index-D-9tzkhM.js → index-qCVVoBzB.js} +1 -1
- package/dist/assets/{index-DHwEE904.js → index-r7RBj2Zs.js} +9854 -9870
- package/dist/assets/{index-BG4MEhH2.js → index-sfVh4bD9.js} +1 -1
- package/dist/assets/{lexical-DPC-NxTc.js → lexical-DTfvcVgU.js} +1 -1
- package/dist/assets/{liveblocks-BUJqCtmZ.js → liveblocks-Bl3A2p2B.js} +1 -1
- package/dist/assets/{lottie-D8AaXarT.js → lottie-B-MkWC6q.js} +1 -1
- package/dist/assets/{mui-BD9ReF4V.js → mui-axh11eoI.js} +1 -1
- package/dist/assets/{pathToGoBack-D3dTZRn8.js → pathToGoBack-DfPLhZwj.js} +1 -1
- package/dist/assets/{pdf-iZy3i289.js → pdf-VFwJrwPY.js} +3 -3
- package/dist/assets/{pdf-lib-C-qCwtZm.js → pdf-lib-B51MNmfW.js} +1 -1
- package/dist/assets/{plaid-CxNli5Mg.js → plaid-BUPrZO5b.js} +1 -1
- package/dist/assets/{pusher-0ovRh4wm.js → pusher-CAQ8WE0X.js} +1 -1
- package/dist/assets/{react-DSIBkdUb.js → react-Be8LqDD0.js} +1 -1
- package/dist/assets/{react-DqIQJ9MY.js → react-CFX24kGb.js} +1 -1
- package/dist/assets/{react-Bt99sb5m.js → react-CVjxRdIQ.js} +1 -1
- package/dist/assets/{react-Db_3nqMy.js → react-DEAMftpa.js} +1 -1
- package/dist/assets/{react-Cd-2CZq4.js → react-DbTUBIfc.js} +1 -1
- package/dist/assets/{react-DhnULN7m.js → react-xXVp9hUr.js} +1 -1
- package/dist/assets/{recharts-DD1DDktN.js → recharts-DUgaG0ww.js} +1 -1
- package/dist/assets/{routePaths-BGzy8Dqj.js → routePaths-CuMG5DGQ.js} +1 -1
- package/dist/assets/{sentry-BbjYSgze.js → sentry-DDBUQl1l.js} +1 -1
- package/dist/assets/{url-BaKOjXZM.js → url-DIsZgeBv.js} +1 -1
- package/dist/assets/{url-DYDNdwVU.js → url-iKcR75LE.js} +1 -1
- package/dist/assets/{useAskAiCfoHostNavButtonProps-CIdYxL8V.js → useAskAiCfoHostNavButtonProps-BZNd8NRL.js} +1 -1
- package/dist/assets/{useDeviceId-DZSzfiw7.js → useDeviceId-BdkLfCQj.js} +1 -1
- package/dist/assets/{useFetchSuggestedQuestionsWhenReady-DaXgh6Pr.js → useFetchSuggestedQuestionsWhenReady-DiJocIaN.js} +1 -1
- package/dist/assets/{useInitialThreadRequest-C6Fi1v7Q.js → useInitialThreadRequest-DDmfCFY0.js} +1 -1
- package/dist/assets/{utils-gH_xiFg7.js → utils-D61OcZ2U.js} +1 -1
- package/dist/assets/{withTransactionSidePanel-C7832Ype.js → withTransactionSidePanel-Rd6FsTPQ.js} +1 -1
- package/dist/assets/{zeni-epic-state-Cdw8EpcM.js → zeni-epic-state-DMCSky0I.js} +3 -3
- package/dist/index.html +1 -1
- package/dist/service-worker.js +88 -896
- package/package.json +2 -2
- package/dist/assets/AddressRoutes-0TGuT2xq.js +0 -1
- package/dist/assets/CustomerOnboardingAuthScreen-Cv8y1n9u.js +0 -1
- package/dist/assets/NotFoundScreen-BHPCFLTM.js +0 -1
- package/dist/assets/TransactionDetailRoutes-BXbmHxwN.js +0 -1
package/dist/service-worker.js
CHANGED
|
@@ -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
|
|
21
|
-
const
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
147
|
-
|
|
74
|
+
if (lastDeployedTime != null && lastDeployed !== lastDeployedTime) {
|
|
75
|
+
lastDeployedTime = lastDeployed;
|
|
76
|
+
updatePending = true;
|
|
148
77
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
}
|