@oneuptime/common 12.0.28 → 12.0.30

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 (367) hide show
  1. package/Models/DatabaseModels/DashboardDomain.ts +43 -0
  2. package/Models/DatabaseModels/Index.ts +11 -0
  3. package/Models/DatabaseModels/MonitorTemplate.ts +15 -3
  4. package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +43 -0
  5. package/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.ts +662 -0
  6. package/Models/DatabaseModels/ProjectOnCallCalendarFeed.ts +584 -0
  7. package/Models/DatabaseModels/StatusPageDomain.ts +43 -0
  8. package/Models/DatabaseModels/UserOnCallCalendarFeed.ts +546 -0
  9. package/Models/DatabaseModels/UserOnCallShiftReminder.ts +231 -0
  10. package/Models/DatabaseModels/UserOnCallShiftReminderLog.ts +331 -0
  11. package/Server/API/DashboardDomainAPI.ts +74 -0
  12. package/Server/API/MicrosoftTeamsAPI.ts +81 -180
  13. package/Server/API/OnCallCalendarAPI.ts +2194 -0
  14. package/Server/API/StatusPageDomainAPI.ts +74 -0
  15. package/Server/EnvironmentConfig.ts +138 -0
  16. package/Server/Infrastructure/OnCallCalendarFeedCache.ts +1050 -0
  17. package/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.ts +43 -0
  18. package/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.ts +40 -0
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.ts +253 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
  21. package/Server/Middleware/OnCallCalendarFeedRateLimit.ts +487 -0
  22. package/Server/Middleware/ProjectAuthorization.ts +6 -1
  23. package/Server/Services/DashboardDomainService.ts +131 -0
  24. package/Server/Services/Index.ts +12 -0
  25. package/Server/Services/LogPipelineProcessorService.ts +88 -0
  26. package/Server/Services/MonitorTemplateService.ts +70 -11
  27. package/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.ts +32 -0
  28. package/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.ts +260 -0
  29. package/Server/Services/OnCallDutyPolicyScheduleLayerService.ts +70 -3
  30. package/Server/Services/OnCallDutyPolicyScheduleLayerUserService.ts +138 -3
  31. package/Server/Services/OnCallDutyPolicyScheduleService.ts +1465 -134
  32. package/Server/Services/OnCallDutyPolicyUserOverrideService.ts +167 -31
  33. package/Server/Services/ProjectOnCallCalendarFeedService.ts +192 -0
  34. package/Server/Services/StatusPageDomainService.ts +163 -3
  35. package/Server/Services/StatusPagePrivateUserSessionService.ts +197 -56
  36. package/Server/Services/TeamMemberService.ts +489 -1
  37. package/Server/Services/TeamService.ts +50 -0
  38. package/Server/Services/UserNotificationSettingService.ts +44 -1
  39. package/Server/Services/UserOnCallCalendarFeedService.ts +159 -0
  40. package/Server/Services/UserOnCallShiftReminderLogService.ts +151 -0
  41. package/Server/Services/UserOnCallShiftReminderService.ts +112 -0
  42. package/Server/Types/Domain.ts +17 -0
  43. package/Server/Utils/APIKey/AccessPermission.ts +24 -0
  44. package/Server/Utils/Greenlock/Greenlock.ts +156 -58
  45. package/Server/Utils/LogPipelineProcessorValidation.ts +112 -0
  46. package/Server/Utils/OnCall/CalendarFeedToken.ts +241 -0
  47. package/Server/Utils/OnCall/OnCallCalendarFeedRenderer.ts +1488 -0
  48. package/Server/Utils/OnCall/OnCallCalendarFeedUrls.ts +230 -0
  49. package/Server/Utils/OnCall/OnCallShiftChangeListeners.ts +227 -0
  50. package/Server/Utils/OnCall/OnCallShiftMaterializer.ts +1127 -0
  51. package/Server/Utils/OnCall/OnCallShiftReminderListener.ts +174 -0
  52. package/Server/Utils/OnCall/OnCallShiftReminderRunner.ts +2584 -0
  53. package/Server/Utils/Response.ts +134 -0
  54. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +1094 -45
  55. package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +153 -17
  56. package/Server/Utils/StartServer.ts +10 -0
  57. package/Server/Utils/Telemetry/AppMetrics.ts +51 -0
  58. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.ts +23 -11
  59. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.ts +0 -170
  60. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.ts +158 -0
  61. package/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.ts +23 -11
  62. package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +35 -13
  63. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +505 -0
  64. package/Tests/App/Dashboard/OnCallCalendarFeedLinks.test.tsx +229 -0
  65. package/Tests/App/Dashboard/OnCallCalendarFeedPlanGate.test.tsx +489 -0
  66. package/Tests/App/Dashboard/OnCallCalendarFeedSideMenus.test.tsx +149 -0
  67. package/Tests/App/Dashboard/OnCallCalendarFeedUtil.test.ts +855 -0
  68. package/Tests/App/Dashboard/OnCallPersonalCalendarFeed.test.tsx +1209 -0
  69. package/Tests/App/Dashboard/OnCallSharedCalendarFeedCard.test.tsx +1019 -0
  70. package/Tests/App/Dashboard/OnCallShiftRemindersAndUpcomingShifts.test.tsx +655 -0
  71. package/Tests/App/Dashboard/UserSettingsSetupChecklistModel.test.ts +73 -0
  72. package/Tests/App/StatusPage/StatusPageLoginCodeBootstrap.test.ts +106 -0
  73. package/Tests/App/StatusPage/StatusPageLoginCodeUtil.test.ts +84 -0
  74. package/Tests/App/StatusPage/StatusPageRedirectSafety.test.ts +119 -0
  75. package/Tests/Models/MonitorTemplateMonitorNameColumn.test.ts +258 -0
  76. package/Tests/Models/OnCallCalendarFeedModels.test.ts +642 -0
  77. package/Tests/Models/OnCallDutyPolicyScheduleCalendarFeed.test.ts +288 -0
  78. package/Tests/Models/OnCallDutyPolicyScheduleShiftConfigVersion.test.ts +88 -0
  79. package/Tests/Models/UserOnCallShiftReminderModels.test.ts +368 -0
  80. package/Tests/Server/API/CustomDomainReissueSslAPI.test.ts +469 -0
  81. package/Tests/Server/API/Helpers.ts +8 -0
  82. package/Tests/Server/API/MicrosoftTeamsBotMessagesEndpoint.test.ts +729 -0
  83. package/Tests/Server/API/MicrosoftTeamsBotTestEndpoint.test.ts +26 -0
  84. package/Tests/Server/API/MicrosoftTeamsLegacyWebhookRemoval.test.ts +402 -0
  85. package/Tests/Server/API/OnCallCalendarAPI.test.ts +4461 -0
  86. package/Tests/Server/EnvironmentConfigOnCallCalendarFeed.test.ts +357 -0
  87. package/Tests/Server/Infrastructure/OnCallCalendarFeedCache.test.ts +1589 -0
  88. package/Tests/Server/Infrastructure/Postgres/AddOnCallCalendarFeedsMigration.test.ts +369 -0
  89. package/Tests/Server/Middleware/OnCallCalendarFeedRateLimit.test.ts +1365 -0
  90. package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +4 -0
  91. package/Tests/Server/Services/AddCertificateReissueRequestedAtToDomainsMigration.test.ts +287 -0
  92. package/Tests/Server/Services/CustomDomainCertificateReissue.test.ts +477 -0
  93. package/Tests/Server/Services/FeedRetentionConsistency.test.ts +70 -0
  94. package/Tests/Server/Services/LogPipelineProcessorSaveValidation.test.ts +249 -0
  95. package/Tests/Server/Services/MonitorTemplateServiceBulkSync.test.ts +549 -0
  96. package/Tests/Server/Services/MonitorTemplateServiceNetworkDeviceSync.test.ts +16 -3
  97. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +57 -0
  98. package/Tests/Server/Services/OnCallCalendarFeedServices.test.ts +1103 -0
  99. package/Tests/Server/Services/OnCallDutyPolicyScheduleResolver.test.ts +1037 -0
  100. package/Tests/Server/Services/OnCallShiftConfigPropagation.test.ts +1270 -0
  101. package/Tests/Server/Services/SecurityEventLastErrorColumnWidth.test.ts +151 -15
  102. package/Tests/Server/Services/StatusPagePrivateUserSessionService.test.ts +587 -0
  103. package/Tests/Server/Services/TeamDeleteMemberCleanup.test.ts +156 -0
  104. package/Tests/Server/Services/TeamMemberOnCallCleanup.test.ts +755 -0
  105. package/Tests/Server/Services/UserNotificationSettingShiftReminderDefaults.test.ts +267 -0
  106. package/Tests/Server/Services/UserOnCallShiftReminderServices.test.ts +583 -0
  107. package/Tests/Server/Types/Database/Permissions/ApiKeyFileAccess.test.ts +286 -0
  108. package/Tests/Server/Types/Database/Permissions/MonitorTemplateColumnAccess.test.ts +328 -0
  109. package/Tests/Server/Types/Database/Permissions/OnCallCalendarFeedPermissions.test.ts +254 -0
  110. package/Tests/Server/Types/Database/QueryHelperFindWithSameTextAndIsNull.test.ts +83 -0
  111. package/Tests/Server/Utils/AI/SRE/ConfidenceSignal.test.ts +377 -0
  112. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +16 -1
  113. package/Tests/Server/Utils/Greenlock/CertificateRenewalSchedule.test.ts +250 -0
  114. package/Tests/Server/Utils/LogPipelineProcessorValidation.test.ts +198 -0
  115. package/Tests/Server/Utils/OnCall/CalendarFeedToken.test.ts +467 -0
  116. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedRenderer.test.ts +2142 -0
  117. package/Tests/Server/Utils/OnCall/OnCallCalendarFeedUrls.test.ts +278 -0
  118. package/Tests/Server/Utils/OnCall/OnCallResolverTestHarness.ts +440 -0
  119. package/Tests/Server/Utils/OnCall/OnCallShiftChangeListeners.test.ts +314 -0
  120. package/Tests/Server/Utils/OnCall/OnCallShiftMaterializer.test.ts +1502 -0
  121. package/Tests/Server/Utils/OnCall/OnCallShiftReminderListener.test.ts +491 -0
  122. package/Tests/Server/Utils/OnCall/OnCallShiftReminderRunner.test.ts +3088 -0
  123. package/Tests/Server/Utils/OnCall/OnCallShiftReminderTestHarness.ts +910 -0
  124. package/Tests/Server/Utils/ResponseSendCalendarResponse.test.ts +561 -0
  125. package/Tests/Server/Utils/SecurityEvent/ConnectorErrorMessage.test.ts +48 -9
  126. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsAuth.test.ts +726 -0
  127. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +74 -11
  128. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsErrorHandling.test.ts +1107 -0
  129. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +20 -3
  130. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +51 -11
  131. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +1246 -0
  132. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsRequestContract.test.ts +961 -0
  133. package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsResponseParsing.test.ts +653 -0
  134. package/Tests/Server/Utils/StartServerEncryptionSecretWarning.test.ts +64 -0
  135. package/Tests/Server/Utils/Telemetry/AppMetrics.test.ts +29 -0
  136. package/Tests/Server/Utils/Workspace/MicrosoftTeamsActionAuthorization.test.ts +880 -0
  137. package/Tests/Types/API/HTTPResponse.test.ts +175 -0
  138. package/Tests/Types/API/Route.test.ts +36 -4
  139. package/Tests/Types/Calendar/ICalendar.test.ts +488 -0
  140. package/Tests/Types/Database/DatabaseProperty.test.ts +171 -0
  141. package/Tests/Types/DateLocalShortDateTimeString.test.ts +518 -0
  142. package/Tests/Types/DateLocalTimeString.test.ts +259 -0
  143. package/Tests/Types/DateUserPrefers12HourFormat.test.ts +316 -0
  144. package/Tests/Types/NotificationSetting/NotificationSettingEventType.test.ts +53 -0
  145. package/Tests/Types/OnCallDutyPolicy/CalendarFeedTestFixtures.ts +310 -0
  146. package/Tests/Types/OnCallDutyPolicy/CalendarFeedWindow.test.ts +205 -0
  147. package/Tests/Types/OnCallDutyPolicy/LayerUtilLayerMetaAndIterationCap.test.ts +458 -0
  148. package/Tests/Types/OnCallDutyPolicy/MaterializedShift.test.ts +197 -0
  149. package/Tests/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.test.ts +1986 -0
  150. package/Tests/Types/OnCallDutyPolicy/ScheduleShiftUtilGroupKey.test.ts +678 -0
  151. package/Tests/Types/OnCallDutyPolicy/ShiftSeamUtil.test.ts +417 -0
  152. package/Tests/UI/Components/LogsHistogram.test.tsx +41 -2
  153. package/Tests/UI/Components/LogsViewerEmptyStateTimeRange.test.tsx +212 -0
  154. package/Tests/UI/Components/TelemetryChartClockFormat.test.tsx +359 -0
  155. package/Tests/UI/Components/TimeRangePickerClockFormat.test.tsx +422 -0
  156. package/Tests/UI/Components/TimeRangePickerDropdown.test.tsx +146 -8
  157. package/Tests/UI/MicrosoftTeams/MicrosoftTeamsMessagingEndpointGuidance.test.tsx +520 -0
  158. package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +10 -0
  159. package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +2 -0
  160. package/Tests/UI/Utils/Navigation.test.ts +82 -0
  161. package/Tests/Utils/Array.test.ts +100 -0
  162. package/Tests/Utils/CertificateReissue.test.ts +225 -0
  163. package/Tests/Utils/Grok/Grok.test.ts +492 -0
  164. package/Tests/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.test.ts +181 -13
  165. package/Types/API/Route.ts +20 -5
  166. package/Types/Calendar/ICalendar.ts +442 -0
  167. package/Types/Date.ts +143 -13
  168. package/Types/Email/EmailTemplateType.ts +2 -0
  169. package/Types/NotificationSetting/NotificationSettingEventType.ts +4 -0
  170. package/Types/OnCallDutyPolicy/CalendarFeedWindow.ts +140 -0
  171. package/Types/OnCallDutyPolicy/Layer.ts +190 -35
  172. package/Types/OnCallDutyPolicy/MaterializedShift.ts +274 -0
  173. package/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.ts +1415 -0
  174. package/Types/OnCallDutyPolicy/ScheduleShiftUtil.ts +159 -4
  175. package/Types/OnCallDutyPolicy/ShiftSeamUtil.ts +106 -0
  176. package/Types/Rum/SessionReplay.ts +62 -1
  177. package/UI/Components/Date/TimeRangePickerDropdown.tsx +9 -5
  178. package/UI/Components/LogsViewer/LogsViewer.tsx +12 -7
  179. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +18 -17
  180. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +21 -28
  181. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +2 -4
  182. package/UI/Components/TelemetryViewer/components/TelemetryHistogram.tsx +2 -4
  183. package/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.tsx +17 -16
  184. package/UI/Utils/Navigation.ts +42 -0
  185. package/Utils/Array.ts +37 -0
  186. package/Utils/CertificateReissue.ts +128 -0
  187. package/Utils/Grok/Grok.ts +461 -0
  188. package/Utils/Grok/GrokPatterns.ts +118 -0
  189. package/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.ts +58 -6
  190. package/build/dist/Models/DatabaseModels/DashboardDomain.js +44 -0
  191. package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
  192. package/build/dist/Models/DatabaseModels/Index.js +11 -0
  193. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  194. package/build/dist/Models/DatabaseModels/MonitorTemplate.js +17 -4
  195. package/build/dist/Models/DatabaseModels/MonitorTemplate.js.map +1 -1
  196. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +44 -0
  197. package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
  198. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js +699 -0
  199. package/build/dist/Models/DatabaseModels/OnCallDutyPolicyScheduleCalendarFeed.js.map +1 -0
  200. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js +620 -0
  201. package/build/dist/Models/DatabaseModels/ProjectOnCallCalendarFeed.js.map +1 -0
  202. package/build/dist/Models/DatabaseModels/StatusPageDomain.js +44 -0
  203. package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
  204. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js +585 -0
  205. package/build/dist/Models/DatabaseModels/UserOnCallCalendarFeed.js.map +1 -0
  206. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js +247 -0
  207. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminder.js.map +1 -0
  208. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js +355 -0
  209. package/build/dist/Models/DatabaseModels/UserOnCallShiftReminderLog.js.map +1 -0
  210. package/build/dist/Server/API/DashboardDomainAPI.js +43 -1
  211. package/build/dist/Server/API/DashboardDomainAPI.js.map +1 -1
  212. package/build/dist/Server/API/MicrosoftTeamsAPI.js +74 -118
  213. package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
  214. package/build/dist/Server/API/OnCallCalendarAPI.js +1371 -0
  215. package/build/dist/Server/API/OnCallCalendarAPI.js.map +1 -0
  216. package/build/dist/Server/API/StatusPageDomainAPI.js +43 -1
  217. package/build/dist/Server/API/StatusPageDomainAPI.js.map +1 -1
  218. package/build/dist/Server/EnvironmentConfig.js +102 -0
  219. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  220. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js +663 -0
  221. package/build/dist/Server/Infrastructure/OnCallCalendarFeedCache.js.map +1 -0
  222. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js +34 -0
  223. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790200000000-AllowNullMonitorNameOnMonitorTemplate.js.map +1 -0
  224. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js +28 -0
  225. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790300000000-AddCertificateReissueRequestedAtToDomains.js.map +1 -0
  226. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js +103 -0
  227. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790400000000-AddOnCallCalendarFeeds.js.map +1 -0
  228. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
  229. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  230. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js +342 -0
  231. package/build/dist/Server/Middleware/OnCallCalendarFeedRateLimit.js.map +1 -0
  232. package/build/dist/Server/Middleware/ProjectAuthorization.js +6 -1
  233. package/build/dist/Server/Middleware/ProjectAuthorization.js.map +1 -1
  234. package/build/dist/Server/Services/DashboardDomainService.js +104 -0
  235. package/build/dist/Server/Services/DashboardDomainService.js.map +1 -1
  236. package/build/dist/Server/Services/Index.js +12 -0
  237. package/build/dist/Server/Services/Index.js.map +1 -1
  238. package/build/dist/Server/Services/LogPipelineProcessorService.js +65 -0
  239. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -1
  240. package/build/dist/Server/Services/MonitorTemplateService.js +57 -11
  241. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  242. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js +20 -0
  243. package/build/dist/Server/Services/OnCallDutyPolicyEscalationRuleScheduleService.js.map +1 -1
  244. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js +219 -0
  245. package/build/dist/Server/Services/OnCallDutyPolicyScheduleCalendarFeedService.js.map +1 -0
  246. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js +47 -3
  247. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerService.js.map +1 -1
  248. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js +112 -3
  249. package/build/dist/Server/Services/OnCallDutyPolicyScheduleLayerUserService.js.map +1 -1
  250. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js +955 -9
  251. package/build/dist/Server/Services/OnCallDutyPolicyScheduleService.js.map +1 -1
  252. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js +93 -6
  253. package/build/dist/Server/Services/OnCallDutyPolicyUserOverrideService.js.map +1 -1
  254. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js +157 -0
  255. package/build/dist/Server/Services/ProjectOnCallCalendarFeedService.js.map +1 -0
  256. package/build/dist/Server/Services/StatusPageDomainService.js +132 -3
  257. package/build/dist/Server/Services/StatusPageDomainService.js.map +1 -1
  258. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js +112 -32
  259. package/build/dist/Server/Services/StatusPagePrivateUserSessionService.js.map +1 -1
  260. package/build/dist/Server/Services/TeamMemberService.js +396 -1
  261. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  262. package/build/dist/Server/Services/TeamService.js +43 -0
  263. package/build/dist/Server/Services/TeamService.js.map +1 -1
  264. package/build/dist/Server/Services/UserNotificationSettingService.js +26 -1
  265. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  266. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js +140 -0
  267. package/build/dist/Server/Services/UserOnCallCalendarFeedService.js.map +1 -0
  268. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js +116 -0
  269. package/build/dist/Server/Services/UserOnCallShiftReminderLogService.js.map +1 -0
  270. package/build/dist/Server/Services/UserOnCallShiftReminderService.js +92 -0
  271. package/build/dist/Server/Services/UserOnCallShiftReminderService.js.map +1 -0
  272. package/build/dist/Server/Types/Domain.js +16 -0
  273. package/build/dist/Server/Types/Domain.js.map +1 -1
  274. package/build/dist/Server/Utils/APIKey/AccessPermission.js +24 -0
  275. package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
  276. package/build/dist/Server/Utils/Greenlock/Greenlock.js +110 -35
  277. package/build/dist/Server/Utils/Greenlock/Greenlock.js.map +1 -1
  278. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js +77 -0
  279. package/build/dist/Server/Utils/LogPipelineProcessorValidation.js.map +1 -0
  280. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js +161 -0
  281. package/build/dist/Server/Utils/OnCall/CalendarFeedToken.js.map +1 -0
  282. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js +983 -0
  283. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedRenderer.js.map +1 -0
  284. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js +155 -0
  285. package/build/dist/Server/Utils/OnCall/OnCallCalendarFeedUrls.js.map +1 -0
  286. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js +145 -0
  287. package/build/dist/Server/Utils/OnCall/OnCallShiftChangeListeners.js.map +1 -0
  288. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js +752 -0
  289. package/build/dist/Server/Utils/OnCall/OnCallShiftMaterializer.js.map +1 -0
  290. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js +92 -0
  291. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderListener.js.map +1 -0
  292. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js +1786 -0
  293. package/build/dist/Server/Utils/OnCall/OnCallShiftReminderRunner.js.map +1 -0
  294. package/build/dist/Server/Utils/Response.js +97 -0
  295. package/build/dist/Server/Utils/Response.js.map +1 -1
  296. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +777 -36
  297. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
  298. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +108 -17
  299. package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
  300. package/build/dist/Server/Utils/StartServer.js +9 -1
  301. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  302. package/build/dist/Server/Utils/Telemetry/AppMetrics.js +41 -0
  303. package/build/dist/Server/Utils/Telemetry/AppMetrics.js.map +1 -1
  304. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js +17 -6
  305. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Alert.js.map +1 -1
  306. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js +0 -159
  307. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Auth.js.map +1 -1
  308. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js +128 -0
  309. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Authorization.js.map +1 -0
  310. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js +17 -6
  311. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/Actions/Incident.js.map +1 -1
  312. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +25 -13
  313. package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
  314. package/build/dist/Types/API/Route.js +16 -5
  315. package/build/dist/Types/API/Route.js.map +1 -1
  316. package/build/dist/Types/Calendar/ICalendar.js +290 -0
  317. package/build/dist/Types/Calendar/ICalendar.js.map +1 -0
  318. package/build/dist/Types/Date.js +115 -13
  319. package/build/dist/Types/Date.js.map +1 -1
  320. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  321. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  322. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js +3 -0
  323. package/build/dist/Types/NotificationSetting/NotificationSettingEventType.js.map +1 -1
  324. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js +97 -0
  325. package/build/dist/Types/OnCallDutyPolicy/CalendarFeedWindow.js.map +1 -0
  326. package/build/dist/Types/OnCallDutyPolicy/Layer.js +113 -19
  327. package/build/dist/Types/OnCallDutyPolicy/Layer.js.map +1 -1
  328. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js +134 -0
  329. package/build/dist/Types/OnCallDutyPolicy/MaterializedShift.js.map +1 -0
  330. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js +800 -0
  331. package/build/dist/Types/OnCallDutyPolicy/OnCallCalendarFeedUtil.js.map +1 -0
  332. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js +97 -3
  333. package/build/dist/Types/OnCallDutyPolicy/ScheduleShiftUtil.js.map +1 -1
  334. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js +77 -0
  335. package/build/dist/Types/OnCallDutyPolicy/ShiftSeamUtil.js.map +1 -0
  336. package/build/dist/Types/Rum/SessionReplay.js +38 -0
  337. package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
  338. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js +9 -5
  339. package/build/dist/UI/Components/Date/TimeRangePickerDropdown.js.map +1 -1
  340. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +12 -7
  341. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  342. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +15 -14
  343. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -1
  344. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +17 -22
  345. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  346. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +2 -4
  347. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -1
  348. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js +2 -4
  349. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogram.js.map +1 -1
  350. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js +15 -14
  351. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryHistogramTooltip.js.map +1 -1
  352. package/build/dist/UI/Utils/Navigation.js +30 -0
  353. package/build/dist/UI/Utils/Navigation.js.map +1 -1
  354. package/build/dist/Utils/Array.js +25 -0
  355. package/build/dist/Utils/Array.js.map +1 -1
  356. package/build/dist/Utils/CertificateReissue.js +97 -0
  357. package/build/dist/Utils/CertificateReissue.js.map +1 -0
  358. package/build/dist/Utils/Grok/Grok.js +324 -0
  359. package/build/dist/Utils/Grok/Grok.js.map +1 -0
  360. package/build/dist/Utils/Grok/GrokPatterns.js +112 -0
  361. package/build/dist/Utils/Grok/GrokPatterns.js.map +1 -0
  362. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js +63 -3
  363. package/build/dist/Utils/Monitor/NetworkDeviceMonitorTemplateUtil.js.map +1 -1
  364. package/package.json +1 -1
  365. package/UI/Utils/JsonWebToken.ts +0 -14
  366. package/build/dist/UI/Utils/JsonWebToken.js +0 -10
  367. package/build/dist/UI/Utils/JsonWebToken.js.map +0 -1
@@ -0,0 +1,1050 @@
1
+ import GlobalCache from "./GlobalCache";
2
+ import InMemoryTTLCache from "./InMemoryTTLCache";
3
+ import logger from "../Utils/Logger";
4
+ import { RENDER_CONCURRENCY } from "../../Types/OnCallDutyPolicy/CalendarFeedWindow";
5
+ import { createHash, randomBytes } from "crypto";
6
+
7
+ /*
8
+ * Caches for the on-call calendar feeds.
9
+ *
10
+ * Three things are cached, at three lifetimes:
11
+ *
12
+ * 1. Schedule segments. One schedule's resolved shifts over a window, shared
13
+ * by every subscriber of that schedule: N personal feeds, the schedule
14
+ * feed and the project feed all read one LayerUtil expansion instead of
15
+ * each doing their own. Apple Calendar's five-minute polls hit this.
16
+ *
17
+ * 2. Feed bodies. The rendered .ics plus its ETag/Last-Modified for one feed,
18
+ * short-lived (the same 300 s the Cache-Control header promises). A
19
+ * client that comes back within the TTL costs one Redis GET.
20
+ *
21
+ * 3. Last-good bodies. The most recent body that rendered successfully for a
22
+ * feed, kept for a day. Served -- with Warning: 110 -- when a render
23
+ * fails, hits the per-process render cap, or trips an iteration cap, so a
24
+ * transient failure degrades to "slightly stale" rather than to a 503
25
+ * that some clients answer by dropping the calendar.
26
+ *
27
+ * Invalidation is by scope generation, not by key scan. GlobalCache has no
28
+ * SCAN, and a SCAN across a Redis that also backs BullMQ is not something a
29
+ * hook should do. Instead every project, user-in-project and schedule has a
30
+ * generation token in Redis; a cached body's real key mixes in the current
31
+ * generations of every scope the feed depends on, so bumping a generation
32
+ * makes every dependent entry unreachable in one write. Old entries age out on
33
+ * their own TTLs. purgeForUser / purgeForSchedule / purgeForProject are those
34
+ * bumps. Last-good bodies are the deliberate exception: their key embeds the
35
+ * feed's own cache key -- which carries the token hash and the candidate
36
+ * schedule set -- and nothing else, so a rotated token or a changed schedule
37
+ * set can never reach one, while a hook-triggered purge does not throw away
38
+ * the only thing standing between a failed render and a 503.
39
+ *
40
+ * Every GlobalCache call is wrapped: each one throws
41
+ * DatabaseNotConnectedException when Redis is down, and the feeds must keep
42
+ * working through that. Reads and writes fall back to a per-process
43
+ * InMemoryTTLCache; generations are written to both so a purge made while
44
+ * Redis is down still takes effect in this process. The in-memory tier is
45
+ * consulted only when Redis fails -- it is never a first-level cache, so a
46
+ * purge made by another process is not masked by a stale local copy.
47
+ *
48
+ * The render slots are the per-process concurrency cap on cache-miss renders.
49
+ * Not Redis-backed on purpose: the thing being protected is THIS process's
50
+ * event loop and heap, and a cap that vanishes when Redis blips would vanish
51
+ * at exactly the moment every request becomes a miss.
52
+ */
53
+
54
+ export const ON_CALL_CALENDAR_FEED_CACHE_NAMESPACE: string = "oncall-calendar";
55
+
56
+ /*
57
+ * The shared window constant is the default; configure() exists so a test (or
58
+ * an operator-facing knob, should one ever be added) can override it per
59
+ * process without touching the pure-types package.
60
+ */
61
+ export const DEFAULT_RENDER_CONCURRENCY: number = RENDER_CONCURRENCY;
62
+
63
+ /*
64
+ * Bodies above this are not cached. The compose Redis has no maxmemory and
65
+ * also backs BullMQ; a handful of multi-megabyte feeds is not what it is
66
+ * sized for. Note that this is NOT the same ceiling as MAX_EVENTS: a VEVENT
67
+ * is roughly 0.9 KB, so 2 MiB is crossed at about 2,200 events while
68
+ * MAX_EVENTS allows 5,000. An hourly rotation over a long window is over this
69
+ * cap and still well inside the event cap; such a feed is re-rendered on
70
+ * every poll.
71
+ */
72
+ export const MAX_CACHED_BODY_BYTES: number = 2 * 1024 * 1024;
73
+
74
+ /*
75
+ * Last-good bodies get their own, larger ceiling. The body cache exists to
76
+ * save work and can afford to skip the heaviest feeds; the last-good tier is
77
+ * the only thing between a failed or capped render and a 503, and refusing it
78
+ * for exactly the feeds most expensive to render -- the ones most likely to
79
+ * trip the render cap -- removed the stale-while-error tier where it matters
80
+ * most. It is written at most once per successful render and read only when
81
+ * something has already gone wrong.
82
+ */
83
+ export const MAX_CACHED_LAST_GOOD_BYTES: number = 8 * 1024 * 1024;
84
+
85
+ /*
86
+ * A schedule's cached segments carry every user's shifts over the window; an
87
+ * hourly rotation over the widest window is several megabytes. Cached, they
88
+ * save the expansion for every subscriber of that schedule, so the ceiling is
89
+ * generous -- but it is a ceiling: an entry above it is returned to the
90
+ * caller and simply not stored, rather than pushed into a Redis with no
91
+ * maxmemory that also backs BullMQ.
92
+ */
93
+ export const MAX_CACHED_SEGMENT_BYTES: number = 8 * 1024 * 1024;
94
+
95
+ /* How long a last-good body is kept, in seconds. */
96
+ export const LAST_GOOD_TTL_SECONDS: number = 24 * 60 * 60;
97
+
98
+ /*
99
+ * Generations are read on every feed request and only ever replaced, never
100
+ * incremented, so they need no TTL at all -- but a key with no TTL is a leak
101
+ * once its schedule or user is deleted. 30 days comfortably exceeds every
102
+ * cache TTL that depends on them; an expired generation reads as the default
103
+ * and simply starts a fresh key space.
104
+ */
105
+ const GENERATION_TTL_SECONDS: number = 30 * 24 * 60 * 60;
106
+
107
+ /* Generation value when none has ever been written for a scope. */
108
+ const DEFAULT_GENERATION: string = "0";
109
+
110
+ const MEMORY_MAX_ENTRIES: number = 500;
111
+ const MEMORY_GENERATION_MAX_ENTRIES: number = 5000;
112
+
113
+ /*
114
+ * The in-process fallback tiers are bounded by BYTES as well as by entries.
115
+ * They are only written while Redis is failing, but what they hold then are
116
+ * whole calendar bodies and whole schedule expansions -- megabytes each -- so
117
+ * an entry count alone bounds nothing: 500 multi-megabyte strings is most of
118
+ * a container's heap. Entries also outlive their TTL (nothing reads a
119
+ * fallback tier once Redis is healthy again, so nothing triggers the
120
+ * expire-on-read), which is why the store sweeps expired entries when it
121
+ * needs room.
122
+ */
123
+ export const MEMORY_MAX_BYTES: number = 32 * 1024 * 1024;
124
+
125
+ /*
126
+ * What a feed body depends on, for invalidation. projectId is always known
127
+ * (every feed is per project). userId is set for a personal feed.
128
+ * scheduleIds are the candidate schedules the body was rendered from --
129
+ * every schedule for a schedule/project feed, the user's candidates for a
130
+ * personal one.
131
+ */
132
+ export interface OnCallCalendarFeedCacheScope {
133
+ projectId: string;
134
+ userId?: string | undefined;
135
+ scheduleIds?: Array<string> | undefined;
136
+ }
137
+
138
+ /* One schedule's slot in a batched segment read. */
139
+ export interface ScheduleSegmentsCacheEntry {
140
+ scheduleId: string;
141
+ /* Everything the value depends on except the schedule's generation. */
142
+ key: string;
143
+ }
144
+
145
+ /* A rendered feed as the response helper wants it. */
146
+ export interface CachedCalendarBody {
147
+ body: string;
148
+ etag: string;
149
+ lastModified: Date;
150
+ }
151
+
152
+ /* The wire form of CachedCalendarBody (Date as ISO 8601). */
153
+ interface SerializedCalendarBody {
154
+ body: string;
155
+ etag: string;
156
+ lastModified: string;
157
+ }
158
+
159
+ interface MemoryEntry {
160
+ value: string;
161
+ bytes: number;
162
+ expiresAt: number;
163
+ }
164
+
165
+ /*
166
+ * The in-process fallback store: InMemoryTTLCache's coarse-LRU behaviour plus
167
+ * a byte budget and a sweep. Not a change to InMemoryTTLCache itself -- every
168
+ * other user of that class caches small values and does not need the
169
+ * accounting (SessionReplayGateCacheStore keeps its own bounded store for the
170
+ * same reason).
171
+ */
172
+ class MemoryStringStore {
173
+ private store: Map<string, MemoryEntry> = new Map();
174
+ private bytes: number = 0;
175
+
176
+ public constructor(
177
+ private maxEntries: number,
178
+ private maxBytes: number,
179
+ ) {}
180
+
181
+ public set(key: string, value: string, ttlMs: number): void {
182
+ this.delete(key);
183
+
184
+ const bytes: number = Buffer.byteLength(value, "utf8");
185
+
186
+ /* A single value over the whole budget is not worth evicting for. */
187
+ if (bytes > this.maxBytes) {
188
+ return;
189
+ }
190
+
191
+ /*
192
+ * Sweep on every write. Nothing reads these tiers while Redis is healthy,
193
+ * so expire-on-read never fires for them: without this, an entry written
194
+ * during an outage stays resident long past its TTL, until 500 newer
195
+ * writes push it out. A write only happens while Redis is failing and the
196
+ * store holds at most a few hundred entries, so the scan is free.
197
+ */
198
+ this.sweepExpired();
199
+
200
+ /* Map iteration is insertion order, so the first key is the oldest. */
201
+ while (
202
+ this.store.size > 0 &&
203
+ (this.store.size + 1 > this.maxEntries ||
204
+ this.bytes + bytes > this.maxBytes)
205
+ ) {
206
+ const oldest: string | undefined = this.store.keys().next().value;
207
+
208
+ if (oldest === undefined) {
209
+ break;
210
+ }
211
+
212
+ this.delete(oldest);
213
+ }
214
+
215
+ this.store.set(key, { value, bytes, expiresAt: Date.now() + ttlMs });
216
+ this.bytes += bytes;
217
+ }
218
+
219
+ public get(key: string): string | undefined {
220
+ const entry: MemoryEntry | undefined = this.store.get(key);
221
+
222
+ if (!entry) {
223
+ return undefined;
224
+ }
225
+
226
+ if (Date.now() > entry.expiresAt) {
227
+ this.delete(key);
228
+ return undefined;
229
+ }
230
+
231
+ return entry.value;
232
+ }
233
+
234
+ public delete(key: string): void {
235
+ const entry: MemoryEntry | undefined = this.store.get(key);
236
+
237
+ if (!entry) {
238
+ return;
239
+ }
240
+
241
+ this.bytes -= entry.bytes;
242
+ this.store.delete(key);
243
+ }
244
+
245
+ public clear(): void {
246
+ this.store.clear();
247
+ this.bytes = 0;
248
+ }
249
+
250
+ public size(): number {
251
+ return this.store.size;
252
+ }
253
+
254
+ public byteSize(): number {
255
+ return this.bytes;
256
+ }
257
+
258
+ private sweepExpired(): void {
259
+ const now: number = Date.now();
260
+
261
+ for (const [key, entry] of this.store) {
262
+ if (now > entry.expiresAt) {
263
+ this.delete(key);
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ export default class OnCallCalendarFeedCache {
270
+ private static renderConcurrency: number = DEFAULT_RENDER_CONCURRENCY;
271
+ private static activeRenderSlots: number = 0;
272
+
273
+ private static memorySegments: MemoryStringStore = new MemoryStringStore(
274
+ MEMORY_MAX_ENTRIES,
275
+ MEMORY_MAX_BYTES,
276
+ );
277
+ private static memoryBodies: MemoryStringStore = new MemoryStringStore(
278
+ MEMORY_MAX_ENTRIES,
279
+ MEMORY_MAX_BYTES,
280
+ );
281
+ private static memoryLastGood: MemoryStringStore = new MemoryStringStore(
282
+ MEMORY_MAX_ENTRIES,
283
+ MEMORY_MAX_BYTES,
284
+ );
285
+ private static memoryGenerations: InMemoryTTLCache<string> =
286
+ new InMemoryTTLCache<string>(MEMORY_GENERATION_MAX_ENTRIES);
287
+
288
+ /*
289
+ * In-flight renders by effective key, so two requests that miss the same
290
+ * segment key in the same instant share one expansion instead of racing to
291
+ * do it twice. Per process; Redis dedupes nothing here, and does not need
292
+ * to -- the point is not to double the CPU spent on one miss.
293
+ */
294
+ private static inFlightRenders: Map<string, Promise<unknown>> = new Map();
295
+
296
+ // -- Configuration -----------------------------------------------------
297
+
298
+ public static configure(options: { renderConcurrency?: number }): void {
299
+ if (
300
+ options.renderConcurrency !== undefined &&
301
+ Number.isInteger(options.renderConcurrency) &&
302
+ options.renderConcurrency > 0
303
+ ) {
304
+ OnCallCalendarFeedCache.renderConcurrency = options.renderConcurrency;
305
+ }
306
+ }
307
+
308
+ public static getRenderConcurrency(): number {
309
+ return OnCallCalendarFeedCache.renderConcurrency;
310
+ }
311
+
312
+ /*
313
+ * Drop every in-process fallback entry and release every slot. For tests
314
+ * and for nothing else -- a running server has no reason to do this.
315
+ */
316
+ public static clearInProcessState(): void {
317
+ OnCallCalendarFeedCache.memorySegments.clear();
318
+ OnCallCalendarFeedCache.memoryBodies.clear();
319
+ OnCallCalendarFeedCache.memoryLastGood.clear();
320
+ OnCallCalendarFeedCache.memoryGenerations.clear();
321
+ OnCallCalendarFeedCache.inFlightRenders.clear();
322
+ OnCallCalendarFeedCache.activeRenderSlots = 0;
323
+ OnCallCalendarFeedCache.renderConcurrency = DEFAULT_RENDER_CONCURRENCY;
324
+ }
325
+
326
+ // -- Render slots ------------------------------------------------------
327
+
328
+ /*
329
+ * Take a render slot, or refuse. The caller MUST release it in a finally
330
+ * block; a leaked slot is one fewer render this process can do until it
331
+ * restarts.
332
+ */
333
+ public static tryAcquireRenderSlot(
334
+ options?: { leaveFreeSlots?: number | undefined } | undefined,
335
+ ): boolean {
336
+ /*
337
+ * `leaveFreeSlots` keeps a caller out of the last few slots. Session
338
+ * routes (/my-shifts) pass it so they can never take every slot away from
339
+ * the public feed routes, whose callers are calendar clients that answer
340
+ * a 503 with a stale or empty calendar. Clamped so it can never make the
341
+ * budget zero.
342
+ */
343
+ const requestedReserve: number = options?.leaveFreeSlots ?? 0;
344
+
345
+ const reserve: number = Math.min(
346
+ Math.max(0, Math.floor(requestedReserve)),
347
+ OnCallCalendarFeedCache.renderConcurrency - 1,
348
+ );
349
+
350
+ const budget: number = OnCallCalendarFeedCache.renderConcurrency - reserve;
351
+
352
+ if (OnCallCalendarFeedCache.activeRenderSlots >= budget) {
353
+ return false;
354
+ }
355
+
356
+ OnCallCalendarFeedCache.activeRenderSlots++;
357
+
358
+ return true;
359
+ }
360
+
361
+ public static releaseRenderSlot(): void {
362
+ if (OnCallCalendarFeedCache.activeRenderSlots > 0) {
363
+ OnCallCalendarFeedCache.activeRenderSlots--;
364
+ }
365
+ }
366
+
367
+ public static getActiveRenderSlots(): number {
368
+ return OnCallCalendarFeedCache.activeRenderSlots;
369
+ }
370
+
371
+ /* Bytes held by the three in-process fallback tiers. Tests and diagnostics. */
372
+ public static getInProcessBytes(): number {
373
+ return (
374
+ OnCallCalendarFeedCache.memorySegments.byteSize() +
375
+ OnCallCalendarFeedCache.memoryBodies.byteSize() +
376
+ OnCallCalendarFeedCache.memoryLastGood.byteSize()
377
+ );
378
+ }
379
+
380
+ // -- Schedule-level segment cache --------------------------------------
381
+
382
+ /*
383
+ * Read one schedule's resolved segments for `key`, rendering and storing
384
+ * them on a miss. `key` should already carry everything the result depends
385
+ * on (the shiftConfigVersion, the window, the day bucket); the schedule's
386
+ * generation is mixed in here so purgeForSchedule invalidates it.
387
+ *
388
+ * The render function's result must survive JSON.stringify/JSON.parse --
389
+ * Dates should be ISO strings or epoch numbers. The value handed back on a
390
+ * hit is the parsed JSON, so callers must not rely on class instances.
391
+ *
392
+ * A render that throws is not cached and the error propagates; a cache that
393
+ * throws is treated as a miss.
394
+ */
395
+ public static async getOrRenderScheduleSegments<T>(data: {
396
+ scheduleId: string;
397
+ key: string;
398
+ ttlSeconds: number;
399
+ render: () => Promise<T>;
400
+ }): Promise<T> {
401
+ const rendered: Map<string, T> =
402
+ await OnCallCalendarFeedCache.getOrRenderScheduleSegmentsBatch<T>({
403
+ entries: [{ scheduleId: data.scheduleId, key: data.key }],
404
+ ttlSeconds: data.ttlSeconds,
405
+ renderMissing: async (
406
+ missing: Array<ScheduleSegmentsCacheEntry>,
407
+ ): Promise<Map<string, T>> => {
408
+ const out: Map<string, T> = new Map();
409
+
410
+ if (missing.length > 0) {
411
+ out.set(data.scheduleId, await data.render());
412
+ }
413
+
414
+ return out;
415
+ },
416
+ });
417
+
418
+ const value: T | undefined = rendered.get(data.scheduleId);
419
+
420
+ if (value === undefined) {
421
+ throw new Error(
422
+ "OnCallCalendarFeedCache: the schedule segment render produced no value.",
423
+ );
424
+ }
425
+
426
+ return value;
427
+ }
428
+
429
+ /*
430
+ * The same thing for MANY schedules at once, which is how a feed render
431
+ * actually reads: a project feed wants every schedule in the project, a
432
+ * personal feed every schedule its subscriber is on.
433
+ *
434
+ * Reading them one at a time meant one materialization per schedule, and
435
+ * each of those is a schedule query, three IN queries, an override query
436
+ * and a users/project lookup -- roughly seven round trips PER SCHEDULE,
437
+ * with the same user and project rows re-read every time, all fanned out
438
+ * concurrently against a fifty-connection pool. `renderMissing` is called
439
+ * ONCE with every schedule that missed, so the batched resolver behind it
440
+ * does that work in a handful of IN queries for the whole feed.
441
+ *
442
+ * Hits, in-flight renders (a schedule another feed is already expanding)
443
+ * and misses are separated first; only the misses reach `renderMissing`,
444
+ * which MUST return an entry for every id it is given. Each result is
445
+ * stored under its own key, so a schedule expanded for one feed is a hit
446
+ * for the next.
447
+ */
448
+ public static async getOrRenderScheduleSegmentsBatch<T>(data: {
449
+ entries: Array<ScheduleSegmentsCacheEntry>;
450
+ ttlSeconds: number;
451
+ renderMissing: (
452
+ missing: Array<ScheduleSegmentsCacheEntry>,
453
+ ) => Promise<Map<string, T>>;
454
+ }): Promise<Map<string, T>> {
455
+ const result: Map<string, T> = new Map();
456
+
457
+ /* One entry per schedule; a repeated id is one read and one render. */
458
+ const entries: Array<ScheduleSegmentsCacheEntry> = [];
459
+ const seen: Set<string> = new Set();
460
+
461
+ for (const entry of data.entries) {
462
+ if (seen.has(entry.scheduleId)) {
463
+ continue;
464
+ }
465
+
466
+ seen.add(entry.scheduleId);
467
+ entries.push(entry);
468
+ }
469
+
470
+ if (entries.length === 0) {
471
+ return result;
472
+ }
473
+
474
+ const effectiveKeys: Map<string, string> = new Map();
475
+
476
+ await Promise.all(
477
+ entries.map(async (entry: ScheduleSegmentsCacheEntry): Promise<void> => {
478
+ const generation: string = await OnCallCalendarFeedCache.getGeneration(
479
+ OnCallCalendarFeedCache.scheduleGenerationKey(entry.scheduleId),
480
+ );
481
+
482
+ effectiveKeys.set(
483
+ entry.scheduleId,
484
+ `seg:${OnCallCalendarFeedCache.digest(
485
+ `${entry.scheduleId}|${generation}|${entry.key}`,
486
+ )}`,
487
+ );
488
+ }),
489
+ );
490
+
491
+ const reads: Array<{
492
+ entry: ScheduleSegmentsCacheEntry;
493
+ parsed: T | undefined;
494
+ }> = await Promise.all(
495
+ entries.map(
496
+ async (
497
+ entry: ScheduleSegmentsCacheEntry,
498
+ ): Promise<{
499
+ entry: ScheduleSegmentsCacheEntry;
500
+ parsed: T | undefined;
501
+ }> => {
502
+ const cached: string | null =
503
+ await OnCallCalendarFeedCache.readString(
504
+ effectiveKeys.get(entry.scheduleId) as string,
505
+ OnCallCalendarFeedCache.memorySegments,
506
+ );
507
+
508
+ return {
509
+ entry,
510
+ parsed:
511
+ cached === null
512
+ ? undefined
513
+ : OnCallCalendarFeedCache.parseJson<T>(cached),
514
+ };
515
+ },
516
+ ),
517
+ );
518
+
519
+ const missing: Array<ScheduleSegmentsCacheEntry> = [];
520
+ const joining: Array<ScheduleSegmentsCacheEntry> = [];
521
+
522
+ /*
523
+ * From here to the in-flight registration below there is NO await: a
524
+ * concurrent caller must not be able to look at the in-flight map between
525
+ * this check and the registration, or two requests that missed the same
526
+ * key in the same instant would both render it.
527
+ */
528
+ for (const read of reads) {
529
+ if (read.parsed !== undefined) {
530
+ result.set(read.entry.scheduleId, read.parsed);
531
+ continue;
532
+ }
533
+
534
+ if (
535
+ OnCallCalendarFeedCache.inFlightRenders.has(
536
+ effectiveKeys.get(read.entry.scheduleId) as string,
537
+ )
538
+ ) {
539
+ joining.push(read.entry);
540
+ continue;
541
+ }
542
+
543
+ missing.push(read.entry);
544
+ }
545
+
546
+ const registered: Array<string> = [];
547
+
548
+ let renderPromise: Promise<Map<string, T>> | null = null;
549
+
550
+ if (missing.length > 0) {
551
+ renderPromise = (async (): Promise<Map<string, T>> => {
552
+ const rendered: Map<string, T> = await data.renderMissing(missing);
553
+
554
+ await Promise.all(
555
+ missing.map(
556
+ async (entry: ScheduleSegmentsCacheEntry): Promise<void> => {
557
+ const value: T | undefined = rendered.get(entry.scheduleId);
558
+
559
+ if (value === undefined) {
560
+ return;
561
+ }
562
+
563
+ const serialized: string = JSON.stringify(value);
564
+
565
+ if (
566
+ !OnCallCalendarFeedCache.isWithinLimit(
567
+ serialized,
568
+ MAX_CACHED_SEGMENT_BYTES,
569
+ "schedule segment",
570
+ )
571
+ ) {
572
+ return;
573
+ }
574
+
575
+ await OnCallCalendarFeedCache.writeString(
576
+ effectiveKeys.get(entry.scheduleId) as string,
577
+ serialized,
578
+ data.ttlSeconds,
579
+ OnCallCalendarFeedCache.memorySegments,
580
+ );
581
+ },
582
+ ),
583
+ );
584
+
585
+ return rendered;
586
+ })();
587
+
588
+ for (const entry of missing) {
589
+ const effectiveKey: string = effectiveKeys.get(
590
+ entry.scheduleId,
591
+ ) as string;
592
+
593
+ const view: Promise<T | undefined> = renderPromise.then(
594
+ (rendered: Map<string, T>): T | undefined => {
595
+ return rendered.get(entry.scheduleId);
596
+ },
597
+ );
598
+
599
+ /*
600
+ * A view nobody joins must not become an unhandled rejection when the
601
+ * batch fails; the failure still reaches everyone who awaits it.
602
+ */
603
+ view.catch((): void => {});
604
+
605
+ OnCallCalendarFeedCache.inFlightRenders.set(effectiveKey, view);
606
+ registered.push(effectiveKey);
607
+ }
608
+ }
609
+
610
+ try {
611
+ await Promise.all([
612
+ ...joining.map(
613
+ async (entry: ScheduleSegmentsCacheEntry): Promise<void> => {
614
+ const inFlight: Promise<unknown> | undefined =
615
+ OnCallCalendarFeedCache.inFlightRenders.get(
616
+ effectiveKeys.get(entry.scheduleId) as string,
617
+ );
618
+
619
+ if (!inFlight) {
620
+ return;
621
+ }
622
+
623
+ const value: T | undefined = (await inFlight) as T | undefined;
624
+
625
+ if (value !== undefined) {
626
+ result.set(entry.scheduleId, value);
627
+ }
628
+ },
629
+ ),
630
+ (async (): Promise<void> => {
631
+ if (!renderPromise) {
632
+ return;
633
+ }
634
+
635
+ const rendered: Map<string, T> = await renderPromise;
636
+
637
+ for (const entry of missing) {
638
+ const value: T | undefined = rendered.get(entry.scheduleId);
639
+
640
+ if (value !== undefined) {
641
+ result.set(entry.scheduleId, value);
642
+ }
643
+ }
644
+ })(),
645
+ ]);
646
+ } finally {
647
+ for (const effectiveKey of registered) {
648
+ OnCallCalendarFeedCache.inFlightRenders.delete(effectiveKey);
649
+ }
650
+ }
651
+
652
+ return result;
653
+ }
654
+
655
+ // -- Body cache --------------------------------------------------------
656
+
657
+ public static async getBody(data: {
658
+ key: string;
659
+ scope: OnCallCalendarFeedCacheScope;
660
+ }): Promise<CachedCalendarBody | null> {
661
+ const effectiveKey: string = await OnCallCalendarFeedCache.bodyKey(
662
+ data.key,
663
+ data.scope,
664
+ );
665
+
666
+ const cached: string | null = await OnCallCalendarFeedCache.readString(
667
+ effectiveKey,
668
+ OnCallCalendarFeedCache.memoryBodies,
669
+ );
670
+
671
+ return OnCallCalendarFeedCache.deserializeBody(cached);
672
+ }
673
+
674
+ /*
675
+ * Store a rendered body. Returns false, and stores nothing, when the body
676
+ * is over MAX_CACHED_BODY_BYTES -- the caller still serves it, it just is
677
+ * not kept.
678
+ */
679
+ public static async setBody(data: {
680
+ key: string;
681
+ scope: OnCallCalendarFeedCacheScope;
682
+ value: CachedCalendarBody;
683
+ ttlSeconds: number;
684
+ }): Promise<boolean> {
685
+ if (
686
+ !OnCallCalendarFeedCache.isWithinLimit(
687
+ data.value.body,
688
+ MAX_CACHED_BODY_BYTES,
689
+ "body",
690
+ )
691
+ ) {
692
+ return false;
693
+ }
694
+
695
+ const effectiveKey: string = await OnCallCalendarFeedCache.bodyKey(
696
+ data.key,
697
+ data.scope,
698
+ );
699
+
700
+ await OnCallCalendarFeedCache.writeString(
701
+ effectiveKey,
702
+ OnCallCalendarFeedCache.serializeBody(data.value),
703
+ data.ttlSeconds,
704
+ OnCallCalendarFeedCache.memoryBodies,
705
+ );
706
+
707
+ return true;
708
+ }
709
+
710
+ // -- Last-good (stale-while-error) -------------------------------------
711
+
712
+ public static async getLastGood(
713
+ key: string,
714
+ ): Promise<CachedCalendarBody | null> {
715
+ const cached: string | null = await OnCallCalendarFeedCache.readString(
716
+ OnCallCalendarFeedCache.lastGoodKey(key),
717
+ OnCallCalendarFeedCache.memoryLastGood,
718
+ );
719
+
720
+ return OnCallCalendarFeedCache.deserializeBody(cached);
721
+ }
722
+
723
+ public static async setLastGood(
724
+ key: string,
725
+ value: CachedCalendarBody,
726
+ ): Promise<boolean> {
727
+ if (
728
+ !OnCallCalendarFeedCache.isWithinLimit(
729
+ value.body,
730
+ MAX_CACHED_LAST_GOOD_BYTES,
731
+ "last-good body",
732
+ )
733
+ ) {
734
+ return false;
735
+ }
736
+
737
+ await OnCallCalendarFeedCache.writeString(
738
+ OnCallCalendarFeedCache.lastGoodKey(key),
739
+ OnCallCalendarFeedCache.serializeBody(value),
740
+ LAST_GOOD_TTL_SECONDS,
741
+ OnCallCalendarFeedCache.memoryLastGood,
742
+ );
743
+
744
+ return true;
745
+ }
746
+
747
+ // -- Purges ------------------------------------------------------------
748
+
749
+ /*
750
+ * Every body cached for this user's personal feed in this project. Called
751
+ * on token rotation, on disable, and when the user leaves the project.
752
+ */
753
+ public static async purgeForUser(
754
+ projectId: string,
755
+ userId: string,
756
+ ): Promise<void> {
757
+ await OnCallCalendarFeedCache.bumpGeneration(
758
+ OnCallCalendarFeedCache.userGenerationKey(projectId, userId),
759
+ );
760
+ }
761
+
762
+ /*
763
+ * The schedule's segment cache and every body that was rendered from it:
764
+ * personal feeds of its members, its own shared feed, the project feed.
765
+ * Called from the layer / layer-user / override / attachment hooks.
766
+ */
767
+ public static async purgeForSchedule(scheduleId: string): Promise<void> {
768
+ await OnCallCalendarFeedCache.bumpGeneration(
769
+ OnCallCalendarFeedCache.scheduleGenerationKey(scheduleId),
770
+ );
771
+ }
772
+
773
+ /*
774
+ * Every body in the project. Called when the project feed is rotated and
775
+ * when a member leaving triggers rotation of the shared feeds.
776
+ */
777
+ public static async purgeForProject(projectId: string): Promise<void> {
778
+ await OnCallCalendarFeedCache.bumpGeneration(
779
+ OnCallCalendarFeedCache.projectGenerationKey(projectId),
780
+ );
781
+ }
782
+
783
+ // -- Keys --------------------------------------------------------------
784
+
785
+ /*
786
+ * The real key of a body: the caller's key plus the current generation of
787
+ * every scope the body depends on, digested so the Redis key stays short
788
+ * whatever the schedule count. Generations are fetched concurrently; a
789
+ * project feed over fifty schedules is fifty-two GETs in one round of
790
+ * awaits, not fifty-two round trips in sequence.
791
+ */
792
+ private static async bodyKey(
793
+ key: string,
794
+ scope: OnCallCalendarFeedCacheScope,
795
+ ): Promise<string> {
796
+ const generationKeys: Array<string> = [
797
+ OnCallCalendarFeedCache.projectGenerationKey(scope.projectId),
798
+ ];
799
+
800
+ if (scope.userId) {
801
+ generationKeys.push(
802
+ OnCallCalendarFeedCache.userGenerationKey(
803
+ scope.projectId,
804
+ scope.userId,
805
+ ),
806
+ );
807
+ }
808
+
809
+ for (const scheduleId of OnCallCalendarFeedCache.uniqueSorted(
810
+ scope.scheduleIds || [],
811
+ )) {
812
+ generationKeys.push(
813
+ OnCallCalendarFeedCache.scheduleGenerationKey(scheduleId),
814
+ );
815
+ }
816
+
817
+ const generations: Array<string> = await Promise.all(
818
+ generationKeys.map((generationKey: string): Promise<string> => {
819
+ return OnCallCalendarFeedCache.getGeneration(generationKey);
820
+ }),
821
+ );
822
+
823
+ return `body:${OnCallCalendarFeedCache.digest(
824
+ `${key}|${generationKeys
825
+ .map((generationKey: string, index: number): string => {
826
+ return `${generationKey}=${generations[index]}`;
827
+ })
828
+ .join("|")}`,
829
+ )}`;
830
+ }
831
+
832
+ private static lastGoodKey(key: string): string {
833
+ return `lastgood:${OnCallCalendarFeedCache.digest(key)}`;
834
+ }
835
+
836
+ private static projectGenerationKey(projectId: string): string {
837
+ return `gen:project:${projectId}`;
838
+ }
839
+
840
+ private static userGenerationKey(projectId: string, userId: string): string {
841
+ return `gen:user:${projectId}:${userId}`;
842
+ }
843
+
844
+ private static scheduleGenerationKey(scheduleId: string): string {
845
+ return `gen:schedule:${scheduleId}`;
846
+ }
847
+
848
+ private static digest(value: string): string {
849
+ return createHash("sha256")
850
+ .update(value, "utf8")
851
+ .digest("hex")
852
+ .slice(0, 40);
853
+ }
854
+
855
+ private static uniqueSorted(values: Array<string>): Array<string> {
856
+ return Array.from(new Set(values)).sort();
857
+ }
858
+
859
+ // -- Generations -------------------------------------------------------
860
+
861
+ /*
862
+ * Redis first; the in-memory copy only when Redis fails. Reading memory
863
+ * first would let a stale local generation hide a purge another process
864
+ * made.
865
+ */
866
+ private static async getGeneration(generationKey: string): Promise<string> {
867
+ try {
868
+ const value: string | null = await GlobalCache.getString(
869
+ ON_CALL_CALENDAR_FEED_CACHE_NAMESPACE,
870
+ generationKey,
871
+ );
872
+
873
+ if (value) {
874
+ return value;
875
+ }
876
+
877
+ return DEFAULT_GENERATION;
878
+ } catch (err) {
879
+ OnCallCalendarFeedCache.logCacheFailure("read generation", err);
880
+
881
+ return (
882
+ OnCallCalendarFeedCache.memoryGenerations.get(generationKey) ||
883
+ DEFAULT_GENERATION
884
+ );
885
+ }
886
+ }
887
+
888
+ /*
889
+ * A fresh random token, not an increment: GlobalCache has no INCR, and a
890
+ * value nobody can predict cannot collide with a stale one. Written to
891
+ * memory unconditionally so a purge made during a Redis outage is honoured
892
+ * by this process straight away; the Redis write is best effort and logged.
893
+ * Never throws -- hooks call this and must not fail over a cache.
894
+ */
895
+ private static async bumpGeneration(generationKey: string): Promise<void> {
896
+ const generation: string = `${Date.now().toString(36)}-${randomBytes(6).toString("hex")}`;
897
+
898
+ OnCallCalendarFeedCache.memoryGenerations.set(
899
+ generationKey,
900
+ generation,
901
+ GENERATION_TTL_SECONDS * 1000,
902
+ );
903
+
904
+ try {
905
+ await GlobalCache.setString(
906
+ ON_CALL_CALENDAR_FEED_CACHE_NAMESPACE,
907
+ generationKey,
908
+ generation,
909
+ { expiresInSeconds: GENERATION_TTL_SECONDS },
910
+ );
911
+ } catch (err) {
912
+ OnCallCalendarFeedCache.logCacheFailure("write generation", err);
913
+ }
914
+ }
915
+
916
+ // -- Storage tiers -----------------------------------------------------
917
+
918
+ private static async readString(
919
+ key: string,
920
+ memory: MemoryStringStore,
921
+ ): Promise<string | null> {
922
+ try {
923
+ return await GlobalCache.getString(
924
+ ON_CALL_CALENDAR_FEED_CACHE_NAMESPACE,
925
+ key,
926
+ );
927
+ } catch (err) {
928
+ OnCallCalendarFeedCache.logCacheFailure("read", err);
929
+
930
+ return memory.get(key) ?? null;
931
+ }
932
+ }
933
+
934
+ private static async writeString(
935
+ key: string,
936
+ value: string,
937
+ ttlSeconds: number,
938
+ memory: MemoryStringStore,
939
+ ): Promise<void> {
940
+ const ttl: number = Math.max(1, Math.floor(ttlSeconds));
941
+
942
+ try {
943
+ await GlobalCache.setString(
944
+ ON_CALL_CALENDAR_FEED_CACHE_NAMESPACE,
945
+ key,
946
+ value,
947
+ { expiresInSeconds: ttl },
948
+ );
949
+ } catch (err) {
950
+ OnCallCalendarFeedCache.logCacheFailure("write", err);
951
+ memory.set(key, value, ttl * 1000);
952
+ }
953
+ }
954
+
955
+ // -- Serialisation -----------------------------------------------------
956
+
957
+ /*
958
+ * Refusals are logged: a feed that is silently never cached looks exactly
959
+ * like a feed that is cached and busy, and the only symptom is a render on
960
+ * every poll.
961
+ */
962
+ private static isWithinLimit(
963
+ value: string,
964
+ maxBytes: number,
965
+ what: string,
966
+ ): boolean {
967
+ const bytes: number = Buffer.byteLength(value, "utf8");
968
+
969
+ if (bytes <= maxBytes) {
970
+ return true;
971
+ }
972
+
973
+ logger.debug(
974
+ `OnCallCalendarFeedCache: not caching a ${what} of ${bytes} bytes (limit ${maxBytes}).`,
975
+ );
976
+
977
+ return false;
978
+ }
979
+
980
+ private static serializeBody(value: CachedCalendarBody): string {
981
+ const serialized: SerializedCalendarBody = {
982
+ body: value.body,
983
+ etag: value.etag,
984
+ lastModified: value.lastModified.toISOString(),
985
+ };
986
+
987
+ return JSON.stringify(serialized);
988
+ }
989
+
990
+ /*
991
+ * A corrupt or foreign entry is a miss, never an error: the worst case is
992
+ * one extra render, which is what a miss costs anyway.
993
+ */
994
+ private static deserializeBody(
995
+ cached: string | null,
996
+ ): CachedCalendarBody | null {
997
+ if (cached === null) {
998
+ return null;
999
+ }
1000
+
1001
+ const parsed: SerializedCalendarBody | undefined =
1002
+ OnCallCalendarFeedCache.parseJson<SerializedCalendarBody>(cached);
1003
+
1004
+ if (
1005
+ !parsed ||
1006
+ typeof parsed !== "object" ||
1007
+ typeof parsed.body !== "string" ||
1008
+ typeof parsed.etag !== "string" ||
1009
+ typeof parsed.lastModified !== "string"
1010
+ ) {
1011
+ return null;
1012
+ }
1013
+
1014
+ const lastModified: Date = new Date(parsed.lastModified);
1015
+
1016
+ if (Number.isNaN(lastModified.getTime())) {
1017
+ return null;
1018
+ }
1019
+
1020
+ return {
1021
+ body: parsed.body,
1022
+ etag: parsed.etag,
1023
+ lastModified,
1024
+ };
1025
+ }
1026
+
1027
+ private static parseJson<T>(value: string): T | undefined {
1028
+ try {
1029
+ return JSON.parse(value) as T;
1030
+ } catch {
1031
+ return undefined;
1032
+ }
1033
+ }
1034
+
1035
+ // -- Logging -----------------------------------------------------------
1036
+
1037
+ /*
1038
+ * Debug, not warn: when Redis is down every feed request takes this path
1039
+ * several times, and Redis being down is already alarmed on by the code
1040
+ * that owns the connection. The message never carries a key -- keys are
1041
+ * digests of token hashes, which is one hop closer to a token than a log
1042
+ * should get.
1043
+ */
1044
+ private static logCacheFailure(operation: string, err: unknown): void {
1045
+ logger.debug(
1046
+ `OnCallCalendarFeedCache: ${operation} failed, using in-process fallback`,
1047
+ );
1048
+ logger.debug(err);
1049
+ }
1050
+ }