diff --git a/benchmark.js b/benchmark.js new file mode 100644 index 0000000..631b493 --- /dev/null +++ b/benchmark.js @@ -0,0 +1,87 @@ +const { performance } = require('perf_hooks'); + +const shiftArrivals = Array.from({ length: 20 }, (_, i) => ({ + flight: { + time: { scheduled: { arrival: 1700000000 + i * 1000 } }, + identification: { number: { default: 'AZ' + i } }, + } +})); + +const shiftDepartures = Array.from({ length: 20 }, (_, i) => ({ + flight: { + time: { scheduled: { departure: 1700000000 + i * 1000 } }, + identification: { number: { default: 'AZ' + i } }, + airline: { name: 'Alitalia' } + } +})); + +const mockScheduleNotificationAsync = async () => { + return new Promise(resolve => setTimeout(() => resolve('id-' + Math.random()), 10)); +}; + +async function sequential() { + const newIds = []; + const now = 1600000000; + + for (const item of shiftArrivals) { + try { + const id = await mockScheduleNotificationAsync(); + newIds.push(id); + } catch (err) {} + } + + for (const item of shiftDepartures) { + try { + const id1 = await mockScheduleNotificationAsync(); + newIds.push(id1); + const id2 = await mockScheduleNotificationAsync(); + newIds.push(id2); + } catch (err) {} + } + return newIds; +} + +async function parallel() { + const now = 1600000000; + const arrivalPromises = shiftArrivals.map(async (item) => { + try { + return await mockScheduleNotificationAsync(); + } catch (err) {} + }); + + const departurePromises = shiftDepartures.map(async (item) => { + try { + const promises = []; + promises.push((async () => { + try { return await mockScheduleNotificationAsync(); } catch (err) {} + })()); + promises.push((async () => { + try { return await mockScheduleNotificationAsync(); } catch (err) {} + })()); + return await Promise.all(promises); + } catch (err) { + return []; + } + }); + + const allResults = await Promise.all([ + ...arrivalPromises, + ...departurePromises + ]); + + return allResults.flat().filter(id => typeof id === 'string'); +} + +async function run() { + const startSeq = performance.now(); + await sequential(); + const endSeq = performance.now(); + console.log(`Sequential: ${(endSeq - startSeq).toFixed(2)}ms`); + + const startPar = performance.now(); + await parallel(); + const endPar = performance.now(); + console.log(`Parallel: ${(endPar - startPar).toFixed(2)}ms`); +} + +run(); diff --git a/package-lock.json b/package-lock.json index a3bbe99..9b54474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@expo/vector-icons": "^15.0.3", "@react-native-async-storage/async-storage": "2.2.0", - "@react-native-picker/picker": "2.11.1", + "@react-native-picker/picker": "2.11.4", "@types/tesseract.js": "^0.0.2", "expo": "~54.0.0", "expo-blur": "~15.0.8", @@ -23,18 +23,19 @@ "expo-linear-gradient": "~15.0.8", "expo-location": "~19.0.8", "expo-notifications": "~0.32.16", + "expo-secure-store": "~15.0.5", "expo-status-bar": "~3.0.9", "react": "19.1.0", "react-native": "0.81.5", "react-native-android-widget": "^0.20.1", "react-native-calendars": "^1.1314.0", - "react-native-webview": "13.15.0", + "react-native-webview": "13.16.1", "tesseract.js": "^7.0.0" }, "devDependencies": { "@react-native-community/cli": "^20.1.3", "@types/react": "~19.1.10", - "pdfjs-dist": "^5.5.207", + "pdfjs-dist": "^5.6.205", "typescript": "~5.9.2" } }, @@ -80,7 +81,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1492,7 +1492,6 @@ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", - "peer": true, "engines": { "node": ">=6.9.0" } @@ -2431,7 +2430,6 @@ "integrity": "sha512-sLo8cu9JyFNfuuF1C+8NJ4DHE/PEFaXGd4enkcxi/OJjGG8+sOQrdjNQ4i+cVh/2c+ah1mEMwsYjc3z0+/MqSg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@react-native-community/cli-clean": "20.1.3", "@react-native-community/cli-config": "20.1.3", @@ -2996,9 +2994,9 @@ } }, "node_modules/@react-native-picker/picker": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.1.tgz", - "integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz", + "integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==", "license": "MIT", "workspaces": [ "example" @@ -3399,7 +3397,6 @@ "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -4109,7 +4106,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5014,7 +5010,6 @@ "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.20.0", "@expo/cli": "54.0.23", @@ -5130,7 +5125,6 @@ "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", "license": "MIT", - "peer": true, "dependencies": { "fontfaceobserver": "^2.1.0" }, @@ -5239,6 +5233,15 @@ "react-native": "*" } }, + "node_modules/expo-secure-store": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz", + "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-server": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", @@ -8412,16 +8415,16 @@ } }, "node_modules/pdfjs-dist": { - "version": "5.5.207", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz", - "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==", + "version": "5.6.205", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.6.205.tgz", + "integrity": "sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=20.19.0 || >=22.13.0 || >=24" }, "optionalDependencies": { - "@napi-rs/canvas": "^0.1.95", + "@napi-rs/canvas": "^0.1.96", "node-readable-to-web-readable-stream": "^0.4.2" } }, @@ -8436,7 +8439,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -8716,7 +8718,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8742,7 +8743,6 @@ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", "license": "MIT", - "peer": true, "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", @@ -8849,11 +8849,10 @@ "license": "MIT" }, "node_modules/react-native-webview": { - "version": "13.15.0", - "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.15.0.tgz", - "integrity": "sha512-Vzjgy8mmxa/JO6l5KZrsTC7YemSdq+qB01diA0FqjUTaWGAGwuykpJ73MDj3+mzBSlaDxAEugHzTtkUQkQEQeQ==", + "version": "13.16.1", + "resolved": "https://registry.npmjs.org/react-native-webview/-/react-native-webview-13.16.1.tgz", + "integrity": "sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==", "license": "MIT", - "peer": true, "dependencies": { "escape-string-regexp": "^4.0.0", "invariant": "2.2.4" @@ -8952,7 +8951,6 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -10211,9 +10209,8 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/utils/autoNotifications.ts b/src/utils/autoNotifications.ts index 4c44518..a98f2b7 100644 --- a/src/utils/autoNotifications.ts +++ b/src/utils/autoNotifications.ts @@ -66,15 +66,19 @@ export async function autoScheduleNotifications(): Promise { // Cancel old and schedule new await cancelPrevious(); const now = Date.now() / 1000; - const newIds: string[] = []; + let newIds: string[] = []; + + // ── PERFORMANCE OPTIMIZATION ── + // Replaced sequential for-of loops with Promise.all and array mapping. + // This allows all the scheduling operations (I/O) to execute concurrently. // ── Arrival notifications: 15 min before landing ── - for (const item of shiftArrivals) { + const arrivalPromises = shiftArrivals.map(async (item: any) => { try { const arrTs: number | undefined = item.flight?.time?.scheduled?.arrival; - if (!arrTs || isNaN(arrTs)) continue; + if (!arrTs || isNaN(arrTs)) return null; const secondsUntilNotify = arrTs - 15 * 60 - now; - if (secondsUntilNotify <= 0 || isNaN(secondsUntilNotify)) continue; + if (secondsUntilNotify <= 0 || isNaN(secondsUntilNotify)) return null; const flightNumber = item.flight?.identification?.number?.default || 'N/A'; const airline = item.flight?.airline?.name || 'Sconosciuta'; @@ -82,7 +86,7 @@ export async function autoScheduleNotifications(): Promise { || item.flight?.airport?.origin?.code?.iata || 'N/A'; const arrivalTime = new Date(arrTs * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); - const id = await Notifications.scheduleNotificationAsync({ + return await Notifications.scheduleNotificationAsync({ content: { title: `✈️ Arrivo tra 15 min — ${flightNumber}`, body: `${airline} da ${origin} · arrivo alle ${arrivalTime}`, @@ -91,17 +95,17 @@ export async function autoScheduleNotifications(): Promise { }, trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilNotify), repeats: false }, }); - newIds.push(id); } catch (err) { console.error('Failed to schedule arrival notification:', err); + return null; } - } + }); // ── Departure notifications: check-in open + gate open ── - for (const item of shiftDepartures) { + const departurePromises = shiftDepartures.map(async (item: any) => { try { const depTs: number | undefined = item.flight?.time?.scheduled?.departure; - if (!depTs || isNaN(depTs)) continue; + if (!depTs || isNaN(depTs)) return []; const airline = item.flight?.airline?.name || 'Sconosciuta'; const flightNumber = item.flight?.identification?.number?.default || 'N/A'; @@ -111,22 +115,31 @@ export async function autoScheduleNotifications(): Promise { // Get airline-specific ops times const ops = getAirlineOps(airline); + const flightNotificationPromises: Promise[] = []; // Notification at check-in open (e.g. 2h before departure) const ciOpenTs = depTs - ops.checkInOpen * 60; const secondsUntilCI = ciOpenTs - now; if (secondsUntilCI > 0 && !isNaN(secondsUntilCI)) { const ciTime = new Date(ciOpenTs * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); - const id = await Notifications.scheduleNotificationAsync({ - content: { - title: `📌 Check-in aperto — ${flightNumber}`, - body: `${airline} per ${destination} · partenza ${depTime} · CI dalle ${ciTime}`, - sound: true, - data: { flightNumber, depTs, type: 'checkin_open' }, - }, - trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCI), repeats: false }, - }); - newIds.push(id); + flightNotificationPromises.push( + (async () => { + try { + return await Notifications.scheduleNotificationAsync({ + content: { + title: `📌 Check-in aperto — ${flightNumber}`, + body: `${airline} per ${destination} · partenza ${depTime} · CI dalle ${ciTime}`, + sound: true, + data: { flightNumber, depTs, type: 'checkin_open' }, + }, + trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilCI), repeats: false }, + }); + } catch (err) { + console.error('Failed to schedule check-in notification:', err); + return null; + } + })() + ); } // Notification at gate open @@ -134,36 +147,70 @@ export async function autoScheduleNotifications(): Promise { const secondsUntilGate = gateOpenTs - now; if (secondsUntilGate > 0 && !isNaN(secondsUntilGate)) { const gateTime = new Date(gateOpenTs * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); - const id = await Notifications.scheduleNotificationAsync({ - content: { - title: `🚪 Gate aperto — ${flightNumber}`, - body: `${airline} per ${destination} · gate dalle ${gateTime} · partenza ${depTime}`, - sound: true, - data: { flightNumber, depTs, type: 'gate_open' }, - }, - trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGate), repeats: false }, - }); - newIds.push(id); + flightNotificationPromises.push( + (async () => { + try { + return await Notifications.scheduleNotificationAsync({ + content: { + title: `🚪 Gate aperto — ${flightNumber}`, + body: `${airline} per ${destination} · gate dalle ${gateTime} · partenza ${depTime}`, + sound: true, + data: { flightNumber, depTs, type: 'gate_open' }, + }, + trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilGate), repeats: false }, + }); + } catch (err) { + console.error('Failed to schedule gate notification:', err); + return null; + } + })() + ); } + + return await Promise.all(flightNotificationPromises); } catch (err) { console.error('Failed to schedule departure notification:', err); + return []; } - } + }); - // Shift end notification + // Shift end notification promise + let shiftEndPromise: Promise = Promise.resolve(null); const secondsUntilEnd = shiftEnd - now; if (secondsUntilEnd > 0) { const endTime = new Date(shiftEnd * 1000).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' }); - const endId = await Notifications.scheduleNotificationAsync({ - content: { - title: 'Turno terminato', - body: `Buon lavoro! Il tuo turno delle ${endTime} è concluso.`, - sound: true, - data: { type: 'shift_end' }, - }, - trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilEnd), repeats: false }, - }); - newIds.push(endId); + shiftEndPromise = (async () => { + try { + return await Notifications.scheduleNotificationAsync({ + content: { + title: 'Turno terminato', + body: `Buon lavoro! Il tuo turno delle ${endTime} è concluso.`, + sound: true, + data: { type: 'shift_end' }, + }, + trigger: { type: Notifications.SchedulableTriggerInputTypes.TIME_INTERVAL, seconds: Math.round(secondsUntilEnd), repeats: false }, + }); + } catch (err) { + console.error('Failed to schedule shift end notification:', err); + return null; + } + })(); + } + + // Await all scheduling operations concurrently + const [arrivalResults, departureResults, shiftEndResult] = await Promise.all([ + Promise.all(arrivalPromises), + Promise.all(departurePromises), + shiftEndPromise + ]); + + // Extract successful string IDs, flatten departure array of arrays, and remove nulls + newIds = [ + ...arrivalResults.filter((id): id is string => typeof id === 'string'), + ...departureResults.flat().filter((id): id is string => typeof id === 'string') + ]; + if (typeof shiftEndResult === 'string') { + newIds.push(shiftEndResult); } await AsyncStorage.setItem(NOTIF_IDS_KEY, JSON.stringify(newIds));