tiny-essentials 1.28.19 → 1.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/changelog/1/28/19.md +24 -0
- package/changelog/1/29/0.md +58 -0
- package/dist/v1/ColorSafeStringify.min.js +1 -1
- package/dist/v1/TinyAdvancedRaffle.min.js +1 -1
- package/dist/v1/TinyAfterScrollWatcher.min.js +1 -1
- package/dist/v1/TinyAnalogClock.min.js +1 -1
- package/dist/v1/TinyArrayComparator.min.js +1 -1
- package/dist/v1/TinyArrayPaginator.min.js +1 -1
- package/dist/v1/TinyBasicsEs.min.js +1 -1
- package/dist/v1/TinyBrowserMonitor.min.js +1 -0
- package/dist/v1/TinyClassManager.min.js +1 -1
- package/dist/v1/TinyClipboard.min.js +1 -1
- package/dist/v1/TinyCloner.min.js +1 -0
- package/dist/v1/TinyColorConverter.min.js +1 -1
- package/dist/v1/TinyColorValidator.min.js +1 -1
- package/dist/v1/TinyCookieConsent.min.js +1 -1
- package/dist/v1/TinyDayNightCycle.min.js +1 -1
- package/dist/v1/TinyDebugger.min.js +1 -1
- package/dist/v1/TinyDomReadyManager.min.js +1 -1
- package/dist/v1/TinyDragDropDetector.min.js +1 -1
- package/dist/v1/TinyDragger.min.js +1 -1
- package/dist/v1/TinyElementObserver.min.js +1 -1
- package/dist/v1/TinyEssentials.min.js +1 -1
- package/dist/v1/TinyGamepad.min.js +1 -1
- package/dist/v1/TinyHtml.min.js +1 -1
- package/dist/v1/TinyHtmlTagRegexBuilder.min.js +1 -0
- package/dist/v1/TinyHttpResponseRegistry.min.js +1 -0
- package/dist/v1/TinyI18.min.js +1 -1
- package/dist/v1/TinyIframeEvents.min.js +1 -1
- package/dist/v1/TinyInventory.min.js +1 -1
- package/dist/v1/TinyInventoryTrader.min.js +1 -1
- package/dist/v1/TinyLevelUp.min.js +1 -1
- package/dist/v1/TinyLoadingScreen.min.js +1 -1
- package/dist/v1/TinyLocalStorage.min.js +1 -1
- package/dist/v1/TinyMaInSys.min.js +1 -1
- package/dist/v1/TinyMapCache.min.js +1 -0
- package/dist/v1/TinyMediaPlayer.min.js +1 -1
- package/dist/v1/TinyNeedBar.min.js +1 -1
- package/dist/v1/TinyNewWinEvents.min.js +1 -1
- package/dist/v1/TinyNotificationAdapter.min.js +1 -0
- package/dist/v1/TinyNotifications.min.js +1 -1
- package/dist/v1/TinyNotifyCenter.min.js +1 -1
- package/dist/v1/TinyOPFSFileSystem.min.js +1 -0
- package/dist/v1/TinyPasswordValidator.min.js +1 -0
- package/dist/v1/TinyPluginPackage.min.js +1 -0
- package/dist/v1/TinyPromiseQueue.min.js +1 -1
- package/dist/v1/TinyPushManager.min.js +1 -0
- package/dist/v1/TinyPushPayload.min.js +1 -0
- package/dist/v1/TinyPushRouter.min.js +1 -0
- package/dist/v1/TinyRadioFm.min.js +1 -1
- package/dist/v1/TinyRateLimiter.min.js +1 -1
- package/dist/v1/TinyRegexEs.min.js +1 -0
- package/dist/v1/TinyRouter.min.js +1 -0
- package/dist/v1/TinyServiceWorker.min.js +1 -0
- package/dist/v1/TinyServiceWorkerEngine.min.js +1 -0
- package/dist/v1/TinySetMapPackage.min.js +1 -0
- package/dist/v1/TinySimpleDice.min.js +1 -1
- package/dist/v1/TinySmartScroller.min.js +1 -1
- package/dist/v1/TinyStorageBucketFileSystem.min.js +1 -0
- package/dist/v1/TinyTextDiffer.min.js +1 -1
- package/dist/v1/TinyTextRangeEditor.min.js +1 -1
- package/dist/v1/TinyTextarea.min.js +1 -1
- package/dist/v1/TinyThrottledApi.min.js +1 -0
- package/dist/v1/TinyTimeout.min.js +1 -1
- package/dist/v1/TinyToastNotify.min.js +1 -1
- package/dist/v1/TinyUploadClicker.min.js +1 -1
- package/dist/v1/TinyUriParser.min.js +1 -0
- package/dist/v1/TinyVersion.min.js +1 -0
- package/dist/v1/TinyWebWorker.min.js +1 -0
- package/dist/v1/TinyWebWorkerEngine.min.js +1 -0
- package/dist/v1/UltraRandomMsgGen.min.js +1 -1
- package/dist/v1/basics/array.cjs +2 -2
- package/dist/v1/basics/array.d.mts +2 -0
- package/dist/v1/basics/array.d.mts.map +1 -1
- package/dist/v1/basics/array.mjs +2 -1
- package/dist/v1/basics/browserDetector.cjs +37 -12
- package/dist/v1/basics/browserDetector.d.mts +8 -0
- package/dist/v1/basics/browserDetector.d.mts.map +1 -1
- package/dist/v1/basics/browserDetector.mjs +32 -13
- package/dist/v1/basics/crypto.cjs +23 -0
- package/dist/v1/basics/crypto.d.mts +10 -0
- package/dist/v1/basics/crypto.d.mts.map +1 -0
- package/dist/v1/basics/crypto.mjs +18 -0
- package/dist/v1/basics/html.cjs +0 -3
- package/dist/v1/basics/html.d.mts.map +1 -1
- package/dist/v1/basics/html.mjs +0 -2
- package/dist/v1/basics/index.cjs +8 -1
- package/dist/v1/basics/index.d.mts +6 -5
- package/dist/v1/basics/index.d.mts.map +1 -1
- package/dist/v1/basics/index.mjs +6 -5
- package/dist/v1/basics/objChecker.cjs +88 -0
- package/dist/v1/basics/objChecker.d.mts +26 -0
- package/dist/v1/basics/objChecker.d.mts.map +1 -1
- package/dist/v1/basics/objChecker.mjs +74 -0
- package/dist/v1/basics/objFilter.cjs +234 -93
- package/dist/v1/basics/objFilter.d.mts +96 -22
- package/dist/v1/basics/objFilter.d.mts.map +1 -1
- package/dist/v1/basics/objFilter.mjs +216 -92
- package/dist/v1/basics/promiseUtils.cjs +48 -12
- package/dist/v1/basics/promiseUtils.d.mts +45 -12
- package/dist/v1/basics/promiseUtils.d.mts.map +1 -1
- package/dist/v1/basics/promiseUtils.mjs +47 -12
- package/dist/v1/basics/text.cjs +0 -12
- package/dist/v1/basics/text.d.mts +0 -10
- package/dist/v1/basics/text.d.mts.map +1 -1
- package/dist/v1/basics/text.mjs +0 -10
- package/dist/v1/index.cjs +88 -3
- package/dist/v1/index.d.mts +31 -6
- package/dist/v1/index.d.mts.map +1 -1
- package/dist/v1/index.mjs +32 -7
- package/dist/v1/jsdoc/EventEmitter.cjs +4 -0
- package/dist/v1/jsdoc/EventEmitter.d.mts +33 -0
- package/dist/v1/jsdoc/EventEmitter.d.mts.map +1 -0
- package/dist/v1/jsdoc/EventEmitter.mjs +18 -0
- package/dist/v1/libs/array/TinyArrayComparator.cjs +240 -25
- package/dist/v1/libs/array/TinyArrayComparator.d.mts +130 -22
- package/dist/v1/libs/array/TinyArrayComparator.d.mts.map +1 -1
- package/dist/v1/libs/array/TinyArrayComparator.mjs +210 -24
- package/dist/v1/libs/fs/constants.cjs +74 -0
- package/dist/v1/libs/fs/constants.d.mts +22 -0
- package/dist/v1/libs/fs/constants.d.mts.map +1 -0
- package/dist/v1/libs/fs/constants.mjs +53 -0
- package/dist/v1/libs/fs/encoding.cjs +118 -0
- package/dist/v1/libs/fs/encoding.d.mts +50 -0
- package/dist/v1/libs/fs/encoding.d.mts.map +1 -0
- package/dist/v1/libs/fs/encoding.mjs +102 -0
- package/dist/v1/libs/fs/error.cjs +188 -0
- package/dist/v1/libs/fs/error.d.mts +105 -0
- package/dist/v1/libs/fs/error.d.mts.map +1 -0
- package/dist/v1/libs/fs/error.mjs +158 -0
- package/dist/v1/libs/fs/handle.cjs +121 -0
- package/dist/v1/libs/fs/handle.d.mts +56 -0
- package/dist/v1/libs/fs/handle.d.mts.map +1 -0
- package/dist/v1/libs/fs/handle.mjs +100 -0
- package/dist/v1/libs/fs/index.cjs +700 -0
- package/dist/v1/libs/fs/index.d.mts +377 -0
- package/dist/v1/libs/fs/index.d.mts.map +1 -0
- package/dist/v1/libs/fs/index.mjs +630 -0
- package/dist/v1/libs/fs/path.cjs +234 -0
- package/dist/v1/libs/fs/path.d.mts +176 -0
- package/dist/v1/libs/fs/path.d.mts.map +1 -0
- package/dist/v1/libs/fs/path.mjs +197 -0
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.cjs +202 -0
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts +121 -0
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.d.mts.map +1 -0
- package/dist/v1/libs/fs/plugins/OPFS/StorageBucket.mjs +172 -0
- package/dist/v1/libs/fs/plugins/OPFS/index.cjs +334 -0
- package/dist/v1/libs/fs/plugins/OPFS/index.d.mts +98 -0
- package/dist/v1/libs/fs/plugins/OPFS/index.d.mts.map +1 -0
- package/dist/v1/libs/fs/plugins/OPFS/index.mjs +300 -0
- package/dist/v1/libs/fs/stats.cjs +245 -0
- package/dist/v1/libs/fs/stats.d.mts +226 -0
- package/dist/v1/libs/fs/stats.d.mts.map +1 -0
- package/dist/v1/libs/fs/stats.mjs +197 -0
- package/dist/v1/libs/html/events/TinyIframeEvents.cjs +10 -1
- package/dist/v1/libs/html/events/TinyIframeEvents.d.mts +9 -2
- package/dist/v1/libs/html/events/TinyIframeEvents.d.mts.map +1 -1
- package/dist/v1/libs/html/events/TinyIframeEvents.mjs +8 -1
- package/dist/v1/libs/html/events/TinyNewWinEvents.cjs +14 -3
- package/dist/v1/libs/html/events/TinyNewWinEvents.d.mts +21 -5
- package/dist/v1/libs/html/events/TinyNewWinEvents.d.mts.map +1 -1
- package/dist/v1/libs/html/events/TinyNewWinEvents.mjs +12 -3
- package/dist/v1/libs/html/scroll/TinySmartScroller.cjs +13 -1
- package/dist/v1/libs/html/scroll/TinySmartScroller.d.mts +29 -2
- package/dist/v1/libs/html/scroll/TinySmartScroller.d.mts.map +1 -1
- package/dist/v1/libs/html/scroll/TinySmartScroller.mjs +11 -1
- package/dist/v1/libs/math/TinyAdvancedRaffle.cjs +13 -1
- package/dist/v1/libs/math/TinyAdvancedRaffle.d.mts +32 -2
- package/dist/v1/libs/math/TinyAdvancedRaffle.d.mts.map +1 -1
- package/dist/v1/libs/math/TinyAdvancedRaffle.mjs +11 -1
- package/dist/v1/libs/math/TinyTimeout.cjs +0 -14
- package/dist/v1/libs/math/TinyTimeout.d.mts +0 -11
- package/dist/v1/libs/math/TinyTimeout.d.mts.map +1 -1
- package/dist/v1/libs/math/TinyTimeout.mjs +0 -13
- package/dist/v1/libs/media/TinyMediaPlayer/index.cjs +25 -1
- package/dist/v1/libs/media/TinyMediaPlayer/index.d.mts +35 -2
- package/dist/v1/libs/media/TinyMediaPlayer/index.d.mts.map +1 -1
- package/dist/v1/libs/media/TinyMediaPlayer/index.mjs +23 -1
- package/dist/v1/libs/media/TinyMediaPlayer.cjs +2 -2
- package/dist/v1/libs/media/TinyMediaPlayer.d.mts +2 -2
- package/dist/v1/libs/media/TinyMediaPlayer.d.mts.map +1 -1
- package/dist/v1/libs/media/TinyMediaPlayer.mjs +2 -2
- package/dist/v1/libs/media/TinyRadioFm.cjs +19 -1
- package/dist/v1/libs/media/TinyRadioFm.d.mts +78 -2
- package/dist/v1/libs/media/TinyRadioFm.d.mts.map +1 -1
- package/dist/v1/libs/media/TinyRadioFm.mjs +17 -1
- package/dist/v1/libs/plugin/TinyPlugin.cjs +1630 -0
- package/dist/v1/libs/plugin/TinyPlugin.d.mts +753 -0
- package/dist/v1/libs/plugin/TinyPlugin.d.mts.map +1 -0
- package/dist/v1/libs/plugin/TinyPlugin.mjs +1376 -0
- package/dist/v1/libs/plugin/TinyVersion.cjs +204 -0
- package/dist/v1/libs/plugin/TinyVersion.d.mts +70 -0
- package/dist/v1/libs/plugin/TinyVersion.d.mts.map +1 -0
- package/dist/v1/libs/plugin/TinyVersion.mjs +179 -0
- package/dist/v1/libs/router/TinyRouter.cjs +661 -0
- package/dist/v1/libs/router/TinyRouter.d.mts +320 -0
- package/dist/v1/libs/router/TinyRouter.d.mts.map +1 -0
- package/dist/v1/libs/router/TinyRouter.mjs +572 -0
- package/dist/v1/libs/storage/TinyLocalStorage.cjs +20 -1
- package/dist/v1/libs/storage/TinyLocalStorage.d.mts +64 -2
- package/dist/v1/libs/storage/TinyLocalStorage.d.mts.map +1 -1
- package/dist/v1/libs/storage/TinyLocalStorage.mjs +18 -1
- package/dist/v1/libs/storage/TinyMapCache.cjs +373 -0
- package/dist/v1/libs/storage/TinyMapCache.d.mts +274 -0
- package/dist/v1/libs/storage/TinyMapCache.d.mts.map +1 -0
- package/dist/v1/libs/storage/TinyMapCache.mjs +333 -0
- package/dist/v1/libs/storage/TinySetMapDatabase.cjs +1167 -0
- package/dist/v1/libs/storage/TinySetMapDatabase.d.mts +603 -0
- package/dist/v1/libs/storage/TinySetMapDatabase.d.mts.map +1 -0
- package/dist/v1/libs/storage/TinySetMapDatabase.mjs +1059 -0
- package/dist/v1/libs/sw/browser/TinyNotificationAdapter.cjs +126 -0
- package/dist/v1/libs/sw/browser/TinyNotificationAdapter.d.mts +132 -0
- package/dist/v1/libs/sw/browser/TinyNotificationAdapter.d.mts.map +1 -0
- package/dist/v1/libs/sw/browser/TinyNotificationAdapter.mjs +109 -0
- package/dist/v1/libs/sw/browser/TinyPushManager.cjs +310 -0
- package/dist/v1/libs/sw/browser/TinyPushManager.d.mts +168 -0
- package/dist/v1/libs/sw/browser/TinyPushManager.d.mts.map +1 -0
- package/dist/v1/libs/sw/browser/TinyPushManager.mjs +264 -0
- package/dist/v1/libs/sw/browser/TinyServiceWorker.cjs +1060 -0
- package/dist/v1/libs/sw/browser/TinyServiceWorker.d.mts +348 -0
- package/dist/v1/libs/sw/browser/TinyServiceWorker.d.mts.map +1 -0
- package/dist/v1/libs/sw/browser/TinyServiceWorker.mjs +933 -0
- package/dist/v1/libs/sw/browser/plugins/TabManager.cjs +315 -0
- package/dist/v1/libs/sw/browser/plugins/TabManager.d.mts +178 -0
- package/dist/v1/libs/sw/browser/plugins/TabManager.d.mts.map +1 -0
- package/dist/v1/libs/sw/browser/plugins/TabManager.mjs +274 -0
- package/dist/v1/libs/sw/browser/utils.cjs +28 -0
- package/dist/v1/libs/sw/browser/utils.d.mts +8 -0
- package/dist/v1/libs/sw/browser/utils.d.mts.map +1 -0
- package/dist/v1/libs/sw/browser/utils.mjs +21 -0
- package/dist/v1/libs/sw/service/TinyPushRouter.cjs +222 -0
- package/dist/v1/libs/sw/service/TinyPushRouter.d.mts +138 -0
- package/dist/v1/libs/sw/service/TinyPushRouter.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/TinyPushRouter.mjs +201 -0
- package/dist/v1/libs/sw/service/TinyServiceWorkerEngine.cjs +3016 -0
- package/dist/v1/libs/sw/service/TinyServiceWorkerEngine.d.mts +993 -0
- package/dist/v1/libs/sw/service/TinyServiceWorkerEngine.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/TinyServiceWorkerEngine.mjs +2558 -0
- package/dist/v1/libs/sw/service/plugins/GlobBypassPlugin.cjs +88 -0
- package/dist/v1/libs/sw/service/plugins/GlobBypassPlugin.d.mts +32 -0
- package/dist/v1/libs/sw/service/plugins/GlobBypassPlugin.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/plugins/GlobBypassPlugin.mjs +69 -0
- package/dist/v1/libs/sw/service/plugins/GlobCachePlugin.cjs +112 -0
- package/dist/v1/libs/sw/service/plugins/GlobCachePlugin.d.mts +31 -0
- package/dist/v1/libs/sw/service/plugins/GlobCachePlugin.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/plugins/GlobCachePlugin.mjs +91 -0
- package/dist/v1/libs/sw/service/plugins/Ping.cjs +32 -0
- package/dist/v1/libs/sw/service/plugins/Ping.d.mts +9 -0
- package/dist/v1/libs/sw/service/plugins/Ping.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/plugins/Ping.mjs +25 -0
- package/dist/v1/libs/sw/service/plugins/TabManager.cjs +457 -0
- package/dist/v1/libs/sw/service/plugins/TabManager.d.mts +91 -0
- package/dist/v1/libs/sw/service/plugins/TabManager.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/plugins/TabManager.mjs +372 -0
- package/dist/v1/libs/sw/service/plugins/ViteFileDetector.cjs +180 -0
- package/dist/v1/libs/sw/service/plugins/ViteFileDetector.d.mts +28 -0
- package/dist/v1/libs/sw/service/plugins/ViteFileDetector.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/plugins/ViteFileDetector.mjs +148 -0
- package/dist/v1/libs/sw/service/shared/TinyPushPayload.cjs +199 -0
- package/dist/v1/libs/sw/service/shared/TinyPushPayload.d.mts +80 -0
- package/dist/v1/libs/sw/service/shared/TinyPushPayload.d.mts.map +1 -0
- package/dist/v1/libs/sw/service/shared/TinyPushPayload.mjs +174 -0
- package/dist/v1/libs/sw/utils.cjs +57 -0
- package/dist/v1/libs/sw/utils.d.mts +173 -0
- package/dist/v1/libs/sw/utils.d.mts.map +1 -0
- package/dist/v1/libs/sw/utils.mjs +50 -0
- package/dist/v1/libs/text/TinyI18/Browser-NON-DOM.cjs +57 -0
- package/dist/v1/libs/text/TinyI18/Browser-NON-DOM.d.mts +39 -0
- package/dist/v1/libs/text/TinyI18/Browser-NON-DOM.d.mts.map +1 -0
- package/dist/v1/libs/text/TinyI18/Browser-NON-DOM.mjs +48 -0
- package/dist/v1/libs/text/TinyI18/Browser.cjs +64 -0
- package/dist/v1/libs/text/TinyI18/Browser.d.mts +47 -0
- package/dist/v1/libs/text/TinyI18/Browser.d.mts.map +1 -0
- package/dist/v1/libs/text/TinyI18/Browser.mjs +57 -0
- package/dist/v1/libs/text/TinyI18/Node.cjs +49 -0
- package/dist/v1/libs/text/TinyI18/Node.d.mts +38 -0
- package/dist/v1/libs/text/TinyI18/Node.d.mts.map +1 -0
- package/dist/v1/libs/text/TinyI18/Node.mjs +43 -0
- package/dist/v1/libs/text/TinyI18/browser-utils.cjs +17 -0
- package/dist/v1/libs/text/TinyI18/browser-utils.d.mts +8 -0
- package/dist/v1/libs/text/TinyI18/browser-utils.d.mts.map +1 -0
- package/dist/v1/libs/text/TinyI18/browser-utils.mjs +13 -0
- package/dist/v1/libs/text/{TinyI18.cjs → TinyI18/index.cjs} +205 -104
- package/dist/v1/libs/text/{TinyI18.d.mts → TinyI18/index.d.mts} +141 -92
- package/dist/v1/libs/text/TinyI18/index.d.mts.map +1 -0
- package/dist/v1/libs/text/{TinyI18.mjs → TinyI18/index.mjs} +195 -101
- package/dist/v1/libs/tools/TinyBrowserMonitor.cjs +1088 -0
- package/dist/v1/libs/tools/TinyBrowserMonitor.d.mts +621 -0
- package/dist/v1/libs/tools/TinyBrowserMonitor.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyBrowserMonitor.mjs +940 -0
- package/dist/v1/libs/tools/TinyDebugger.cjs +180 -19
- package/dist/v1/libs/tools/TinyDebugger.d.mts +220 -24
- package/dist/v1/libs/tools/TinyDebugger.d.mts.map +1 -1
- package/dist/v1/libs/tools/TinyDebugger.mjs +161 -19
- package/dist/v1/libs/tools/TinyHtmlTagRegexBuilder.cjs +256 -0
- package/dist/v1/libs/tools/TinyHtmlTagRegexBuilder.d.mts +125 -0
- package/dist/v1/libs/tools/TinyHtmlTagRegexBuilder.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHtmlTagRegexBuilder.mjs +223 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser-NON-DOM.cjs +23 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser-NON-DOM.d.mts +17 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser-NON-DOM.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser-NON-DOM.mjs +19 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser.cjs +23 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser.d.mts +17 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Browser.mjs +19 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/EmptyRequestCodes.cjs +390 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/EmptyRequestCodes.d.mts +9 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/EmptyRequestCodes.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/EmptyRequestCodes.mjs +387 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/JsDoc.cjs +2 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/JsDoc.d.mts +27 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/JsDoc.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/JsDoc.mjs +12 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/MozillaRequestCodes/LICENSE +53 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/MozillaRequestCodes/index.cjs +405 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/MozillaRequestCodes/index.d.mts +34 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/MozillaRequestCodes/index.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/MozillaRequestCodes/index.mjs +342 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Node.cjs +23 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Node.d.mts +17 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Node.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/Node.mjs +19 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/RequestCodes.cjs +327 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/RequestCodes.d.mts +9 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/RequestCodes.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/RequestCodes.mjs +324 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/index.cjs +268 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/index.d.mts +107 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/index.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyHttpResponseRegistry/index.mjs +238 -0
- package/dist/v1/libs/tools/TinyPasswordValidator.cjs +491 -0
- package/dist/v1/libs/tools/TinyPasswordValidator.d.mts +246 -0
- package/dist/v1/libs/tools/TinyPasswordValidator.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyPasswordValidator.mjs +434 -0
- package/dist/v1/libs/tools/TinyPkgExportValidator.cjs +48 -0
- package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts +14 -0
- package/dist/v1/libs/tools/TinyPkgExportValidator.d.mts.map +1 -1
- package/dist/v1/libs/tools/TinyPkgExportValidator.mjs +44 -0
- package/dist/v1/libs/tools/TinySiteMap.cjs +1029 -0
- package/dist/v1/libs/tools/TinySiteMap.d.mts +429 -0
- package/dist/v1/libs/tools/TinySiteMap.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinySiteMap.mjs +910 -0
- package/dist/v1/libs/tools/TinySiteMapStream.cjs +121 -0
- package/dist/v1/libs/tools/TinySiteMapStream.d.mts +51 -0
- package/dist/v1/libs/tools/TinySiteMapStream.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinySiteMapStream.mjs +85 -0
- package/dist/v1/libs/tools/TinyURLSecurityVerifier.cjs +444 -0
- package/dist/v1/libs/tools/TinyURLSecurityVerifier.d.mts +217 -0
- package/dist/v1/libs/tools/TinyURLSecurityVerifier.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyURLSecurityVerifier.mjs +394 -0
- package/dist/v1/libs/tools/TinyUriParser/BlueSky.cjs +613 -0
- package/dist/v1/libs/tools/TinyUriParser/BlueSky.d.mts +152 -0
- package/dist/v1/libs/tools/TinyUriParser/BlueSky.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyUriParser/BlueSky.mjs +527 -0
- package/dist/v1/libs/tools/TinyUriParser/Discord.cjs +1119 -0
- package/dist/v1/libs/tools/TinyUriParser/Discord.d.mts +306 -0
- package/dist/v1/libs/tools/TinyUriParser/Discord.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyUriParser/Discord.mjs +968 -0
- package/dist/v1/libs/tools/TinyUriParser/MatrixProtocol.cjs +469 -0
- package/dist/v1/libs/tools/TinyUriParser/MatrixProtocol.d.mts +80 -0
- package/dist/v1/libs/tools/TinyUriParser/MatrixProtocol.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyUriParser/MatrixProtocol.mjs +379 -0
- package/dist/v1/libs/tools/TinyUriParser.cjs +217 -0
- package/dist/v1/libs/tools/TinyUriParser.d.mts +109 -0
- package/dist/v1/libs/tools/TinyUriParser.d.mts.map +1 -0
- package/dist/v1/libs/tools/TinyUriParser.mjs +183 -0
- package/dist/v1/libs/utils/TinyCloner.cjs +466 -0
- package/dist/v1/libs/utils/TinyCloner.d.mts +221 -0
- package/dist/v1/libs/utils/TinyCloner.d.mts.map +1 -0
- package/dist/v1/libs/utils/TinyCloner.mjs +416 -0
- package/dist/v1/libs/utils/TinyPromiseQueue.cjs +1 -1
- package/dist/v1/libs/utils/TinyPromiseQueue.d.mts +2 -2
- package/dist/v1/libs/utils/TinyPromiseQueue.d.mts.map +1 -1
- package/dist/v1/libs/utils/TinyPromiseQueue.mjs +1 -1
- package/dist/v1/libs/utils/TinyThrottledApi.cjs +333 -0
- package/dist/v1/libs/utils/TinyThrottledApi.d.mts +163 -0
- package/dist/v1/libs/utils/TinyThrottledApi.d.mts.map +1 -0
- package/dist/v1/libs/utils/TinyThrottledApi.mjs +294 -0
- package/dist/v1/libs/worker/browser/TinyWebWorker.cjs +342 -0
- package/dist/v1/libs/worker/browser/TinyWebWorker.d.mts +97 -0
- package/dist/v1/libs/worker/browser/TinyWebWorker.d.mts.map +1 -0
- package/dist/v1/libs/worker/browser/TinyWebWorker.mjs +305 -0
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.cjs +346 -0
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts +127 -0
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.d.mts.map +1 -0
- package/dist/v1/libs/worker/engine/TinyWebWorkerEngine.mjs +304 -0
- package/dist/v1/regexp/Email.cjs +185 -0
- package/dist/v1/regexp/Email.d.mts +73 -0
- package/dist/v1/regexp/Email.d.mts.map +1 -0
- package/dist/v1/regexp/Email.mjs +153 -0
- package/dist/v1/regexp/Glob.cjs +293 -0
- package/dist/v1/regexp/Glob.d.mts +45 -0
- package/dist/v1/regexp/Glob.d.mts.map +1 -0
- package/dist/v1/regexp/Glob.mjs +260 -0
- package/dist/v1/regexp/Ipv4.cjs +57 -0
- package/dist/v1/regexp/Ipv4.d.mts +29 -0
- package/dist/v1/regexp/Ipv4.d.mts.map +1 -0
- package/dist/v1/regexp/Ipv4.mjs +45 -0
- package/dist/v1/regexp/Ipv6.cjs +67 -0
- package/dist/v1/regexp/Ipv6.d.mts +29 -0
- package/dist/v1/regexp/Ipv6.d.mts.map +1 -0
- package/dist/v1/regexp/Ipv6.mjs +54 -0
- package/dist/v1/regexp/SegmentExtractor.cjs +145 -0
- package/dist/v1/regexp/SegmentExtractor.d.mts +91 -0
- package/dist/v1/regexp/SegmentExtractor.d.mts.map +1 -0
- package/dist/v1/regexp/SegmentExtractor.mjs +116 -0
- package/dist/v1/regexp/UrlDetector.cjs +125 -0
- package/dist/v1/regexp/UrlDetector.d.mts +67 -0
- package/dist/v1/regexp/UrlDetector.d.mts.map +1 -0
- package/dist/v1/regexp/UrlDetector.mjs +102 -0
- package/dist/v1/regexp/Username/index.cjs +233 -0
- package/dist/v1/regexp/Username/index.d.mts +77 -0
- package/dist/v1/regexp/Username/index.d.mts.map +1 -0
- package/dist/v1/regexp/Username/index.mjs +199 -0
- package/dist/v1/regexp/Username/jsDoc.cjs +2 -0
- package/dist/v1/regexp/Username/jsDoc.d.mts +57 -0
- package/dist/v1/regexp/Username/jsDoc.d.mts.map +1 -0
- package/dist/v1/regexp/Username/jsDoc.mjs +18 -0
- package/dist/v1/regexp/Username/templates/BlueSky.cjs +56 -0
- package/dist/v1/regexp/Username/templates/BlueSky.d.mts +35 -0
- package/dist/v1/regexp/Username/templates/BlueSky.d.mts.map +1 -0
- package/dist/v1/regexp/Username/templates/BlueSky.mjs +52 -0
- package/dist/v1/regexp/Username/templates/Discord.cjs +505 -0
- package/dist/v1/regexp/Username/templates/Discord.d.mts +209 -0
- package/dist/v1/regexp/Username/templates/Discord.d.mts.map +1 -0
- package/dist/v1/regexp/Username/templates/Discord.mjs +458 -0
- package/dist/v1/regexp/Username/templates/MatrixProtocol.cjs +143 -0
- package/dist/v1/regexp/Username/templates/MatrixProtocol.d.mts +70 -0
- package/dist/v1/regexp/Username/templates/MatrixProtocol.d.mts.map +1 -0
- package/dist/v1/regexp/Username/templates/MatrixProtocol.mjs +128 -0
- package/dist/v1/regexp/Username/templates/deprecated/Discord.cjs +24 -0
- package/dist/v1/regexp/Username/templates/deprecated/Discord.d.mts +17 -0
- package/dist/v1/regexp/Username/templates/deprecated/Discord.d.mts.map +1 -0
- package/dist/v1/regexp/Username/templates/deprecated/Discord.mjs +20 -0
- package/dist/v1/regexp/Username/templates/index.cjs +11 -0
- package/dist/v1/regexp/Username/templates/index.d.mts +5 -0
- package/dist/v1/regexp/Username/templates/index.d.mts.map +1 -0
- package/dist/v1/regexp/Username/templates/index.mjs +4 -0
- package/dist/v1/regexp/index.cjs +41 -0
- package/dist/v1/regexp/index.d.mts +9 -0
- package/dist/v1/regexp/index.d.mts.map +1 -0
- package/dist/v1/regexp/index.mjs +8 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Browser.cjs +64 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Browser.d.mts +11 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Browser.d.mts.map +1 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Browser.mjs +51 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/FileInput.cjs +66 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/FileInput.d.mts +11 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/FileInput.d.mts.map +1 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/FileInput.mjs +51 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Node.cjs +39 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Node.d.mts +11 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Node.d.mts.map +1 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Node.mjs +28 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Template.cjs +494 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Template.d.mts +142 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Template.d.mts.map +1 -0
- package/dist/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters/Template.mjs +445 -0
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.cjs +30 -4
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.d.mts +5 -2
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.d.mts.map +1 -1
- package/dist/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.mjs +29 -5
- package/dist/v1/webTemplates/media/MediaValidator/v1/Browser/index.cjs +135 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Browser/index.d.mts +83 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Browser/index.d.mts.map +1 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Browser/index.mjs +120 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Node/index.cjs +127 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Node/index.d.mts +95 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Node/index.d.mts.map +1 -0
- package/dist/v1/webTemplates/media/MediaValidator/v1/Node/index.mjs +109 -0
- package/dist/v1/webTemplates/multiformats/14.0/IPFSHasher/index.cjs +83 -0
- package/dist/v1/webTemplates/multiformats/14.0/IPFSHasher/index.d.mts +46 -0
- package/dist/v1/webTemplates/multiformats/14.0/IPFSHasher/index.d.mts.map +1 -0
- package/dist/v1/webTemplates/multiformats/14.0/IPFSHasher/index.mjs +71 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/TinyVitePwaPlugin.cjs +493 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/TinyVitePwaPlugin.d.mts +67 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/TinyVitePwaPlugin.d.mts.map +1 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/TinyVitePwaPlugin.mjs +399 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/githubUtils.cjs +35 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/githubUtils.d.mts +6 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/githubUtils.d.mts.map +1 -0
- package/dist/v1/webTemplates/vite/7.3/plugins/githubUtils.mjs +29 -0
- package/docs/info/RegExp.md +74 -0
- package/docs/v1/Exports.md +101 -0
- package/docs/v1/README.md +113 -15
- package/docs/v1/basics/asyncReplace.md +2 -0
- package/docs/v1/basics/browserDetector.md +28 -11
- package/docs/v1/basics/crypto.md +85 -0
- package/docs/v1/basics/deprecated/html.md +2 -0
- package/docs/v1/basics/deprecated/objFilter.md +2 -0
- package/docs/v1/basics/html.md +2 -0
- package/docs/v1/basics/objChecker.md +55 -0
- package/docs/v1/basics/objFilter.md +57 -86
- package/docs/v1/basics/promiseUtils.md +12 -3
- package/docs/v1/basics/text.md +0 -1
- package/docs/v1/libs/array/TinyArrayComparator.md +50 -39
- package/docs/v1/libs/fs/TinyFS.md +752 -0
- package/docs/v1/libs/fs/TinyOPFSFileSystem.md +495 -0
- package/docs/v1/libs/fs/TinyStorageBucketFileSystem.md +407 -0
- package/docs/v1/libs/plugin/TinyPlugin.md +441 -0
- package/docs/v1/libs/plugin/TinyVersion.md +145 -0
- package/docs/v1/libs/router/TinyRouter.md +216 -0
- package/docs/v1/libs/storage/TinyMapCache.md +219 -0
- package/docs/v1/libs/storage/TinySetMapDatabase.md +478 -0
- package/docs/v1/libs/sw/browser/TinyNotificationAdapter.md +280 -0
- package/docs/v1/libs/sw/browser/TinyPushManager.md +782 -0
- package/docs/v1/libs/sw/browser/TinyServiceWorker.md +365 -0
- package/docs/v1/libs/sw/browser/plugins/TabManager.md +133 -0
- package/docs/v1/libs/sw/service/TinyPushRouter.md +314 -0
- package/docs/v1/libs/sw/service/TinyServiceWorkerEngine.md +748 -0
- package/docs/v1/libs/sw/service/plugins/GlobBypassPlugin.md +229 -0
- package/docs/v1/libs/sw/service/plugins/GlobCachePlugin.md +91 -0
- package/docs/v1/libs/sw/service/plugins/Ping.md +36 -0
- package/docs/v1/libs/sw/service/plugins/TabManager.md +64 -0
- package/docs/v1/libs/sw/service/plugins/ViteFileDetector.md +92 -0
- package/docs/v1/libs/sw/service/shared/TinyPushPayload.md +316 -0
- package/docs/v1/libs/text/TinyI18.md +80 -56
- package/docs/v1/libs/tools/TinyBrowserMonitor.md +170 -0
- package/docs/v1/libs/tools/TinyDebugger.md +117 -1
- package/docs/v1/libs/tools/TinyHtmlTagRegexBuilder.md +141 -0
- package/docs/v1/libs/tools/TinyHttpResponseRegistry.md +129 -0
- package/docs/v1/libs/tools/TinyPasswordValidator.md +153 -0
- package/docs/v1/libs/tools/TinyPkgExportValidator.md +45 -1
- package/docs/v1/libs/tools/TinySiteMap.md +134 -0
- package/docs/v1/libs/tools/TinySiteMapStream.md +97 -0
- package/docs/v1/libs/tools/TinyURLSecurityVerifier.md +146 -0
- package/docs/v1/libs/tools/TinyUriParser/Discord.md +374 -0
- package/docs/v1/libs/tools/TinyUriParser/MatrixProtocol.md +54 -0
- package/docs/v1/libs/tools/TinyUriParser.md +128 -0
- package/docs/v1/libs/utils/TinyCloner.md +126 -0
- package/docs/v1/libs/utils/TinyPromiseQueue.md +1 -1
- package/docs/v1/libs/utils/TinyThrottledApi.md +219 -0
- package/docs/v1/libs/worker/browser/TinyWebWorker.md +331 -0
- package/docs/v1/libs/worker/engine/TinyWebWorkerEngine.md +137 -0
- package/docs/v1/regexp/Email.md +127 -0
- package/docs/v1/regexp/Glob.md +155 -0
- package/docs/v1/regexp/IPv4.md +103 -0
- package/docs/v1/regexp/IPv6.md +122 -0
- package/docs/v1/regexp/SegmentExtractor.md +120 -0
- package/docs/v1/regexp/UrlDetector.md +117 -0
- package/docs/v1/regexp/Username/Base.md +131 -0
- package/docs/v1/regexp/Username/Discord.md +267 -0
- package/docs/v1/regexp/Username/MatrixProtocol.md +131 -0
- package/docs/v1/webTemplates/ag-psd/31.0/ProcessSolidFilters.md +569 -0
- package/docs/v1/webTemplates/bootstrap/5.3/html/BootstrapDialogs.md +19 -7
- package/docs/v1/webTemplates/media/MediaValidator/v1/Browser.md +123 -0
- package/docs/v1/webTemplates/media/MediaValidator/v1/Node.md +136 -0
- package/docs/v1/webTemplates/multiformats/14.0/IPFSHasher.md +310 -0
- package/docs/v1/webTemplates/vite/7.3/plugins/TinyVitePwaPlugin.md +131 -0
- package/docs/v1/webTemplates/vite/7.3/plugins/githubUtils.md +50 -0
- package/package.json +303 -13
- package/types/EventEmitter.d.ts +48 -0
- package/dist/v1/libs/text/TinyI18.d.mts.map +0 -1
|
@@ -0,0 +1,782 @@
|
|
|
1
|
+
# 📬 TinyPushManager
|
|
2
|
+
|
|
3
|
+
> A tiny, framework-agnostic, zero-dependency wrapper around the browser **Push API**.
|
|
4
|
+
> It owns the full `PushSubscription` lifecycle and keeps your backend in sync.
|
|
5
|
+
|
|
6
|
+
[](#-installation)
|
|
7
|
+
[](#-installation)
|
|
8
|
+
[](#-license)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 📖 Table of Contents
|
|
13
|
+
|
|
14
|
+
- [What is this?](#-what-is-this)
|
|
15
|
+
- [Mental model](#-mental-model)
|
|
16
|
+
- [Installation](#-installation)
|
|
17
|
+
- [Quick start](#-quick-start)
|
|
18
|
+
- [The two files you must not forget](#-the-two-files-you-must-not-forget)
|
|
19
|
+
- [API reference](#-api-reference)
|
|
20
|
+
- [Static members](#static-members)
|
|
21
|
+
- [Instance members](#instance-members)
|
|
22
|
+
- [State machine](#-state-machine)
|
|
23
|
+
- [Type definitions](#-type-definitions)
|
|
24
|
+
- [Recipes](#-recipes)
|
|
25
|
+
- [Vanilla JS](#recipe-1--vanilla-js)
|
|
26
|
+
- [React hook](#recipe-2--react-hook)
|
|
27
|
+
- [Service worker](#recipe-3--service-worker)
|
|
28
|
+
- [Backend contract](#recipe-4--backend-contract)
|
|
29
|
+
- [Security checklist](#-security-checklist)
|
|
30
|
+
- [Apache2 reverse proxy](#-apache2-reverse-proxy)
|
|
31
|
+
- [Troubleshooting](#-troubleshooting)
|
|
32
|
+
- [Testing](#-testing)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## ✨ What is this?
|
|
37
|
+
|
|
38
|
+
`TinyPushManager` is a single class that answers one question:
|
|
39
|
+
|
|
40
|
+
> *"Is this browser subscribed to push notifications, and how do I keep my
|
|
41
|
+
> backend in sync with that answer?"*
|
|
42
|
+
|
|
43
|
+
It does **not** talk to a push service. It never touches FCM, Mozilla
|
|
44
|
+
autopush or APNs. It delegates:
|
|
45
|
+
|
|
46
|
+
| Responsibility | Delegated to |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| Asking the user for permission | `Notification.requestPermission()` |
|
|
49
|
+
| Creating / destroying the subscription | `PushManager` |
|
|
50
|
+
| Persisting the subscription | **Your** `POST` / `DELETE` endpoints |
|
|
51
|
+
| Rendering the notification | **Your** service worker |
|
|
52
|
+
|
|
53
|
+
That is the whole point: the manager is a thin, testable, observable state
|
|
54
|
+
machine on top of a very stateful browser API.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 🧠 Mental model
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
┌───────────────┐
|
|
62
|
+
│ Your UI │
|
|
63
|
+
└───────┬───────┘
|
|
64
|
+
│ 1. subscribe() / unsubscribe()
|
|
65
|
+
▼
|
|
66
|
+
┌───────────────────────┐ 2. PushManager.subscribe()
|
|
67
|
+
│ TinyPushManager │──────────────────────────────▶ 🌐 Browser
|
|
68
|
+
│ (page context only) │◀──────────────────────────────
|
|
69
|
+
└───────┬───────────────┘ 3. PushSubscription
|
|
70
|
+
│
|
|
71
|
+
│ 4. POST /api/push (your backend)
|
|
72
|
+
▼
|
|
73
|
+
┌───────────────┐
|
|
74
|
+
│ Your server │──── 5. web-push / FCM ──▶ 📱 Device
|
|
75
|
+
└───────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Golden rule:** the browser is the source of truth for the subscription.
|
|
79
|
+
Your backend is a *replica*. When they disagree, the browser wins — that is
|
|
80
|
+
what `syncWithServer()` is for.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 📦 Installation
|
|
85
|
+
|
|
86
|
+
The manager is a single ES module with **zero runtime dependencies**.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
src/
|
|
90
|
+
├── push/
|
|
91
|
+
│ ├── tiny-push-manager.mjs # 👈 the class documented here
|
|
92
|
+
│ └── utils.mjs # exports requestNotificationPermission()
|
|
93
|
+
└── utils.mjs # exports PUSH_TYPE
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import TinyPushManager from 'tiny-essentials/libs/sw/browser/TinyPushManager';
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> 📌 **Assumption:** `PUSH_TYPE` is a project constant
|
|
101
|
+
> re-exported as `TinyPushManager.PUSH_TYPE`. It is meant to be shared with the
|
|
102
|
+
> service worker so both sides agree on the payload shape.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 🚀 Quick start
|
|
107
|
+
|
|
108
|
+
```js
|
|
109
|
+
import TinyPushManager from 'tiny-essentials/libs/sw/browser/TinyPushManager';
|
|
110
|
+
|
|
111
|
+
// 1. Fail fast with a friendly message.
|
|
112
|
+
if (!TinyPushManager.isSupported()) {
|
|
113
|
+
console.warn('Push notifications are not available in this browser.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Create one manager per page.
|
|
117
|
+
const push = new TinyPushManager({
|
|
118
|
+
registration: await navigator.serviceWorker.ready,
|
|
119
|
+
vapidPublicKey: import.meta.env.VITE_VAPID_PUBLIC_KEY,
|
|
120
|
+
endpoints: {
|
|
121
|
+
subscribe: '/api/push',
|
|
122
|
+
unsubscribe: '/api/push',
|
|
123
|
+
},
|
|
124
|
+
onChange: (state) => {
|
|
125
|
+
document.body.dataset.pushStatus = state.status;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 3. Subscribe. Must be called from a user gesture on iOS Safari.
|
|
130
|
+
document.querySelector('#notify-me').addEventListener('click', async () => {
|
|
131
|
+
try {
|
|
132
|
+
await push.subscribe();
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(error.message);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## 🧩 The two files you must not forgetZ
|
|
142
|
+
|
|
143
|
+
`TinyPushManager.requestPermission()` is a thin, testable wrapper. Keep the
|
|
144
|
+
browser call isolated so it can be mocked:
|
|
145
|
+
|
|
146
|
+
```js
|
|
147
|
+
/**
|
|
148
|
+
* Requests the notification permission from the user.
|
|
149
|
+
*
|
|
150
|
+
* @returns {Promise<NotificationPermission>} The resulting permission.
|
|
151
|
+
*/
|
|
152
|
+
export async function requestNotificationPermission() {
|
|
153
|
+
return Notification.requestPermission();
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 2. `sw.js` — the service worker
|
|
158
|
+
|
|
159
|
+
The manager never renders a notification. If your service worker does not call
|
|
160
|
+
`showNotification()`, Chrome will show a generic "This site has been updated in
|
|
161
|
+
the background" message and may revoke the subscription.
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
self.addEventListener('push', (event) => {
|
|
165
|
+
const payload = event.data ? event.data.json() : {};
|
|
166
|
+
|
|
167
|
+
event.waitUntil(
|
|
168
|
+
self.registration.showNotification(payload.title ?? 'New message', {
|
|
169
|
+
body: payload.body ?? '',
|
|
170
|
+
icon: '/icons/icon-192.png',
|
|
171
|
+
data: { url: payload.url ?? '/' },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
self.addEventListener('notificationclick', (event) => {
|
|
177
|
+
event.notification.close();
|
|
178
|
+
event.waitUntil(self.clients.openWindow(event.notification.data.url));
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 📚 API reference
|
|
185
|
+
|
|
186
|
+
### Static members
|
|
187
|
+
|
|
188
|
+
#### `TinyPushManager.PUSH_TYPE` — `*` *(read-only)*
|
|
189
|
+
|
|
190
|
+
The push payload discriminator. Use it in the
|
|
191
|
+
service worker to ignore messages that are not yours.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
if (payload.type !== PUSH_TYPE) return;
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
#### `TinyPushManager.isSupported()` → `boolean`
|
|
200
|
+
|
|
201
|
+
Returns `true` only when **all** of the following are true:
|
|
202
|
+
|
|
203
|
+
- `navigator.serviceWorker` exists,
|
|
204
|
+
- `PushManager` exists on `globalThis`,
|
|
205
|
+
- `Notification` exists on `globalThis`.
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
if (!TinyPushManager.isSupported()) {
|
|
209
|
+
// Hide the "Enable notifications" button entirely.
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
#### `TinyPushManager.permission` → `NotificationPermission`
|
|
216
|
+
|
|
217
|
+
A **synchronous** getter for the current permission. Returns `'denied'` when
|
|
218
|
+
the Push API is unsupported, so you can always render it safely.
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
switch (TinyPushManager.permission) {
|
|
222
|
+
case 'granted':
|
|
223
|
+
break;
|
|
224
|
+
case 'denied':
|
|
225
|
+
renderBlockedBanner();
|
|
226
|
+
break;
|
|
227
|
+
default:
|
|
228
|
+
renderOptInButton();
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
#### `TinyPushManager.requestPermission()` → `Promise<NotificationPermission>`
|
|
235
|
+
|
|
236
|
+
| | |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| **Returns** | The resulting permission. |
|
|
239
|
+
| **Throws** | `Error` when the Push API is not supported. |
|
|
240
|
+
|
|
241
|
+
> ⚠️ On iOS Safari this **must** be called from a user gesture, otherwise the
|
|
242
|
+
> promise resolves to `'denied'` and the user never sees a prompt.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
#### `TinyPushManager.decodeVapidKey(base64)` → `BufferSource`
|
|
247
|
+
|
|
248
|
+
Decodes a Base64 **URL-safe** VAPID public key into a `Uint8Array`.
|
|
249
|
+
The result is **memoized**: calling it twice with the same string returns the
|
|
250
|
+
exact same `Uint8Array` instance.
|
|
251
|
+
|
|
252
|
+
| Parameter | Type | Description |
|
|
253
|
+
| --- | --- | --- |
|
|
254
|
+
| `base64` | `string` | A non-empty Base64 URL-safe VAPID public key. |
|
|
255
|
+
|
|
256
|
+
| | |
|
|
257
|
+
| --- | --- |
|
|
258
|
+
| **Returns** | `BufferSource` — the decoded key. |
|
|
259
|
+
| **Throws** | `TypeError` if `base64` is not a non-empty string. |
|
|
260
|
+
| **Throws** | `DOMException` (`InvalidCharacterError`) if the key is not valid Base64. |
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
const key = TinyPushManager.decodeVapidKey(process.env.VAPID_PUBLIC_KEY);
|
|
264
|
+
console.log(key instanceof Uint8Array); // true
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### Instance members
|
|
270
|
+
|
|
271
|
+
#### `new TinyPushManager(options)`
|
|
272
|
+
|
|
273
|
+
| Option | Type | Required | Default | Description |
|
|
274
|
+
| --- | --- | --- | --- | --- |
|
|
275
|
+
| `registration` | `ServiceWorkerRegistration` | ✅ | — | The active registration. |
|
|
276
|
+
| `vapidPublicKey` | `string` | ✅ | — | Base64 URL-safe VAPID **public** key. |
|
|
277
|
+
| `endpoints` | `TinyPushEndpoints` | ❌ | `{}` | Backend routes. |
|
|
278
|
+
| `fetch` | `typeof fetch` | ❌ | `globalThis.fetch` | Injectable for tests. |
|
|
279
|
+
| `onChange` | `(state: TinyPushState) => void` | ❌ | `null` | State observer. |
|
|
280
|
+
|
|
281
|
+
**Throws** `TypeError` if `options` is not a plain object, if `registration` is
|
|
282
|
+
not an object, if `vapidPublicKey` is not a non-empty string, if
|
|
283
|
+
`endpoints.subscribe` / `endpoints.unsubscribe` are not strings, or if
|
|
284
|
+
`onChange` is not a function.
|
|
285
|
+
|
|
286
|
+
```js
|
|
287
|
+
const push = new TinyPushManager({
|
|
288
|
+
registration: await navigator.serviceWorker.ready,
|
|
289
|
+
vapidPublicKey: VAPID_PUBLIC_KEY,
|
|
290
|
+
endpoints: { subscribe: '/api/push', unsubscribe: '/api/push' },
|
|
291
|
+
onChange: ({ status, error }) => console.log(status, error),
|
|
292
|
+
});
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
#### `get state` → `TinyPushState`
|
|
298
|
+
|
|
299
|
+
A **frozen** snapshot of the current state. Mutating it does nothing.
|
|
300
|
+
|
|
301
|
+
```js
|
|
302
|
+
const { status, subscription, error } = push.state;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
#### `getSubscription()` → `Promise<PushSubscription | null>`
|
|
308
|
+
|
|
309
|
+
Reads the subscription from the browser. Returns `null` when the Push API is
|
|
310
|
+
unsupported. **Does not** hit your backend and **does not** change the state.
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
const subscription = await push.getSubscription();
|
|
314
|
+
if (subscription) {
|
|
315
|
+
console.log(subscription.endpoint);
|
|
316
|
+
}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
#### `subscribe(options?)` → `Promise<PushSubscription>`
|
|
322
|
+
|
|
323
|
+
The main entry point. Performs, in order:
|
|
324
|
+
|
|
325
|
+
1. Throws if the Push API is unsupported.
|
|
326
|
+
2. Sets `status` to `'pending'`.
|
|
327
|
+
3. Requests the notification permission.
|
|
328
|
+
4. Sets `status` to `'denied'` and throws if not granted.
|
|
329
|
+
5. Reuses the existing subscription, or creates a new one.
|
|
330
|
+
6. Uploads it to `endpoints.subscribe` (if configured).
|
|
331
|
+
7. Sets `status` to `'subscribed'`.
|
|
332
|
+
|
|
333
|
+
| Parameter | Type | Description |
|
|
334
|
+
| --- | --- | --- |
|
|
335
|
+
| `options` | `PushSubscriptionOptionsInit` | Merged into `pushManager.subscribe()`. |
|
|
336
|
+
|
|
337
|
+
> 🔒 `userVisibleOnly: true` and `applicationServerKey` are **always** forced
|
|
338
|
+
> by the manager and cannot be overridden.
|
|
339
|
+
|
|
340
|
+
| | |
|
|
341
|
+
| --- | --- |
|
|
342
|
+
| **Returns** | The active `PushSubscription`. |
|
|
343
|
+
| **Throws** | `Error` if unsupported, denied, or if the upload fails. |
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
try {
|
|
347
|
+
const subscription = await push.subscribe();
|
|
348
|
+
console.log('Subscribed:', subscription.endpoint);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Could not subscribe:', error.message);
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
---
|
|
355
|
+
|
|
356
|
+
#### `unsubscribe()` → `Promise<boolean>`
|
|
357
|
+
|
|
358
|
+
1. Deletes the subscription on the backend (fire-and-forget, errors swallowed).
|
|
359
|
+
2. Calls `PushSubscription.unsubscribe()`.
|
|
360
|
+
3. Sets `status` to `'unsubscribed'`.
|
|
361
|
+
|
|
362
|
+
| | |
|
|
363
|
+
| --- | --- |
|
|
364
|
+
| **Returns** | `true` when a subscription was removed, `false` otherwise. |
|
|
365
|
+
|
|
366
|
+
> 🧯 Backend errors are **intentionally ignored**. A user must always be able to
|
|
367
|
+
> unsubscribe locally, even when the network is down. Reconcile later with a
|
|
368
|
+
> cron job that prunes `410 Gone` endpoints.
|
|
369
|
+
|
|
370
|
+
```js
|
|
371
|
+
const removed = await push.unsubscribe();
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
#### `syncWithServer()` → `Promise<PushSubscription | null>`
|
|
377
|
+
|
|
378
|
+
Re-uploads the current subscription. Call it:
|
|
379
|
+
|
|
380
|
+
- after a `pushsubscriptionchange` event,
|
|
381
|
+
- after the user logs in on a new device,
|
|
382
|
+
- when your backend lost its database.
|
|
383
|
+
|
|
384
|
+
| | |
|
|
385
|
+
| --- | --- |
|
|
386
|
+
| **Returns** | The refreshed subscription, or `null` if there is none. |
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
await push.syncWithServer();
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
### 🔄 State machine
|
|
395
|
+
|
|
396
|
+
```
|
|
397
|
+
┌────────────────┐
|
|
398
|
+
│ unsubscribed │◀──────────────┐
|
|
399
|
+
└───────┬────────┘ │
|
|
400
|
+
│ subscribe() │
|
|
401
|
+
▼ │
|
|
402
|
+
┌────────────────┐ │
|
|
403
|
+
│ pending │ │
|
|
404
|
+
└───┬────────┬───┘ │
|
|
405
|
+
granted │ │ denied │
|
|
406
|
+
▼ ▼ │
|
|
407
|
+
┌──────────────┐ ┌──────────┐ │
|
|
408
|
+
│ subscribed │ │ denied │ │
|
|
409
|
+
└──────┬───────┘ └──────────┘ │
|
|
410
|
+
│ unsubscribe() │
|
|
411
|
+
└────────────────────────────────┘
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
| From | Call | To | Notes |
|
|
415
|
+
| --- | --- | --- | --- |
|
|
416
|
+
| `unsubscribed` | `subscribe()` | `pending` → `subscribed` | Happy path. |
|
|
417
|
+
| `unsubscribed` | `subscribe()` | `pending` → `denied` | Throws. |
|
|
418
|
+
| `subscribed` | `unsubscribe()` | `unsubscribed` | Returns `true`. |
|
|
419
|
+
| `unsubscribed` | `unsubscribe()` | `unsubscribed` | Returns `false`. |
|
|
420
|
+
| `subscribed` | `syncWithServer()` | `subscribed` | Re-uploads. |
|
|
421
|
+
| any | `subscribe()` + upload failure | `pending` | ⚠️ See [gotchas](#-known-gotchas). |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### 🧾 Type definitions
|
|
426
|
+
|
|
427
|
+
#### `TinyPushEndpoints`
|
|
428
|
+
|
|
429
|
+
| Property | Type | Description |
|
|
430
|
+
| --- | --- | --- |
|
|
431
|
+
| `subscribe` | `string \| undefined` | `POST` route that persists a subscription. |
|
|
432
|
+
| `unsubscribe` | `string \| undefined` | `DELETE` route that removes a subscription. |
|
|
433
|
+
|
|
434
|
+
#### `TinyPushManagerOptions`
|
|
435
|
+
|
|
436
|
+
| Property | Type | Description |
|
|
437
|
+
| --- | --- | --- |
|
|
438
|
+
| `registration` | `ServiceWorkerRegistration` | The active registration. |
|
|
439
|
+
| `vapidPublicKey` | `string` | Base64 URL-safe VAPID public key. |
|
|
440
|
+
| `endpoints` | `TinyPushEndpoints \| undefined` | Backend routes. |
|
|
441
|
+
| `fetch` | `typeof fetch \| undefined` | Injectable for tests. |
|
|
442
|
+
| `onChange` | `((state: TinyPushState) => void) \| undefined` | State observer. |
|
|
443
|
+
|
|
444
|
+
#### `TinyPushState`
|
|
445
|
+
|
|
446
|
+
| Property | Type | Description |
|
|
447
|
+
| --- | --- | --- |
|
|
448
|
+
| `status` | `'unsupported' \| 'unsubscribed' \| 'subscribed' \| 'denied' \| 'pending'` | Current status. |
|
|
449
|
+
| `subscription` | `PushSubscription \| null` | The active subscription, when any. |
|
|
450
|
+
| `error` | `string \| null` | The last error message, when any. |
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 🍳 Recipes
|
|
455
|
+
|
|
456
|
+
### Recipe 1 — Vanilla JS
|
|
457
|
+
|
|
458
|
+
```js
|
|
459
|
+
const push = new TinyPushManager({
|
|
460
|
+
registration: await navigator.serviceWorker.ready,
|
|
461
|
+
vapidPublicKey: VAPID_PUBLIC_KEY,
|
|
462
|
+
endpoints: { subscribe: '/api/push', unsubscribe: '/api/push' },
|
|
463
|
+
onChange: ({ status }) => {
|
|
464
|
+
button.disabled = status === 'pending';
|
|
465
|
+
button.textContent = status === 'subscribed' ? 'Disable' : 'Enable';
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
button.addEventListener('click', async () => {
|
|
470
|
+
const isSubscribed = push.state.status === 'subscribed';
|
|
471
|
+
await (isSubscribed ? push.unsubscribe() : push.subscribe());
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
### Recipe 2 — React hook
|
|
478
|
+
|
|
479
|
+
```jsx
|
|
480
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
481
|
+
import TinyPushManager from 'tiny-essentials/libs/sw/browser/TinyPushManager';
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* @typedef {Object} UsePushResult
|
|
485
|
+
* @property {import('tiny-essentials/libs/sw/browser/TinyPushManager').TinyPushState} state - The manager state.
|
|
486
|
+
* @property {boolean} isSupported - Whether the Push API is available.
|
|
487
|
+
* @property {() => Promise<void>} toggle - Subscribes or unsubscribes.
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Binds a `TinyPushManager` instance to a React component.
|
|
492
|
+
*
|
|
493
|
+
* @param {Object} options - The hook options.
|
|
494
|
+
* @param {string} options.vapidPublicKey - The VAPID public key.
|
|
495
|
+
* @param {string} options.endpoint - The backend route for both verbs.
|
|
496
|
+
* @returns {UsePushResult} The current state and the toggle callback.
|
|
497
|
+
*/
|
|
498
|
+
export function usePush({ vapidPublicKey, endpoint }) {
|
|
499
|
+
const managerRef = useRef(null);
|
|
500
|
+
const [state, setState] = useState({
|
|
501
|
+
status: 'unsubscribed',
|
|
502
|
+
subscription: null,
|
|
503
|
+
error: null,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
useEffect(() => {
|
|
507
|
+
let cancelled = false;
|
|
508
|
+
|
|
509
|
+
navigator.serviceWorker.ready.then((registration) => {
|
|
510
|
+
if (cancelled) return;
|
|
511
|
+
|
|
512
|
+
managerRef.current = new TinyPushManager({
|
|
513
|
+
registration,
|
|
514
|
+
vapidPublicKey,
|
|
515
|
+
endpoints: { subscribe: endpoint, unsubscribe: endpoint },
|
|
516
|
+
onChange: setState,
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
setState(managerRef.current.state);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return () => {
|
|
523
|
+
cancelled = true;
|
|
524
|
+
};
|
|
525
|
+
}, [vapidPublicKey, endpoint]);
|
|
526
|
+
|
|
527
|
+
const toggle = useCallback(async () => {
|
|
528
|
+
const manager = managerRef.current;
|
|
529
|
+
if (!manager) return;
|
|
530
|
+
|
|
531
|
+
if (manager.state.status === 'subscribed') {
|
|
532
|
+
await manager.unsubscribe();
|
|
533
|
+
} else {
|
|
534
|
+
await manager.subscribe();
|
|
535
|
+
}
|
|
536
|
+
}, []);
|
|
537
|
+
|
|
538
|
+
return { state, isSupported: TinyPushManager.isSupported(), toggle };
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Recipe 3 — Service worker
|
|
545
|
+
|
|
546
|
+
Handle `pushsubscriptionchange` so a rotated endpoint never goes stale:
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
self.addEventListener('pushsubscriptionchange', (event) => {
|
|
550
|
+
event.waitUntil(
|
|
551
|
+
(async () => {
|
|
552
|
+
const subscription = await self.registration.pushManager.subscribe(
|
|
553
|
+
event.oldSubscription.options,
|
|
554
|
+
);
|
|
555
|
+
|
|
556
|
+
await fetch('/api/push', {
|
|
557
|
+
method: 'POST',
|
|
558
|
+
headers: { 'Content-Type': 'application/json' },
|
|
559
|
+
body: JSON.stringify(subscription.toJSON()),
|
|
560
|
+
});
|
|
561
|
+
})(),
|
|
562
|
+
);
|
|
563
|
+
});
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
### Recipe 4 — Backend contract
|
|
569
|
+
|
|
570
|
+
#### `POST /api/push`
|
|
571
|
+
|
|
572
|
+
```jsonc
|
|
573
|
+
// Request
|
|
574
|
+
{
|
|
575
|
+
"endpoint": "https://fcm.googleapis.com/fcm/send/abc123",
|
|
576
|
+
"expirationTime": null,
|
|
577
|
+
"keys": {
|
|
578
|
+
"p256dh": "BNc...",
|
|
579
|
+
"auth": "k9F..."
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
| Status | Meaning |
|
|
585
|
+
| --- | --- |
|
|
586
|
+
| `201` | Subscription stored. |
|
|
587
|
+
| `400` | Malformed body. |
|
|
588
|
+
| `401` | Not authenticated. |
|
|
589
|
+
| `429` | Rate limited. |
|
|
590
|
+
|
|
591
|
+
#### `DELETE /api/push`
|
|
592
|
+
|
|
593
|
+
```jsonc
|
|
594
|
+
{ "endpoint": "https://fcm.googleapis.com/fcm/send/abc123" }
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
| Status | Meaning |
|
|
598
|
+
| --- | --- |
|
|
599
|
+
| `204` | Removed (idempotent). |
|
|
600
|
+
| `400` | Malformed body. |
|
|
601
|
+
| `401` | Not authenticated. |
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
## 🔒 Security checklist
|
|
606
|
+
|
|
607
|
+
The manager handles the **browser** half. The backend half is on you.
|
|
608
|
+
|
|
609
|
+
- [ ] **Authenticate both routes.** A session cookie plus a CSRF token, or a
|
|
610
|
+
short-lived Bearer token. An unauthenticated `POST /api/push` is an open
|
|
611
|
+
relay.
|
|
612
|
+
- [ ] **Never trust a `userId` in the body.** Derive the owner from the session
|
|
613
|
+
and store `(user_id, endpoint)` with a `UNIQUE` constraint.
|
|
614
|
+
- [ ] **Validate the endpoint URL.** Reject anything that is not `https:`:
|
|
615
|
+
|
|
616
|
+
```js
|
|
617
|
+
const url = new URL(req.body.endpoint);
|
|
618
|
+
if (url.protocol !== 'https:') {
|
|
619
|
+
return res.status(400).json({ error: 'Invalid endpoint.' });
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
- [ ] **Never `fetch()` a client-supplied endpoint** without an allowlist.
|
|
624
|
+
That is a Server-Side Request Forgery (SSRF) vector.
|
|
625
|
+
- [ ] **Rate limit** by session and by IP. Subscribing is cheap; spamming is not.
|
|
626
|
+
- [ ] **Keep the VAPID private key server-side only.** Only the public key
|
|
627
|
+
reaches the browser, and it is not a secret.
|
|
628
|
+
- [ ] **Prune dead subscriptions.** When a push returns `404` or `410`, delete
|
|
629
|
+
the row.
|
|
630
|
+
- [ ] **Serve over HTTPS.** Service workers require a secure context
|
|
631
|
+
(`localhost` is the only exception).
|
|
632
|
+
- [ ] **Add a Content-Security-Policy** that allows your API origin:
|
|
633
|
+
|
|
634
|
+
```http
|
|
635
|
+
Content-Security-Policy: default-src 'self'; connect-src 'self' https://api.example.com
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## 🪶 Apache2 reverse proxy
|
|
641
|
+
|
|
642
|
+
Enable the required modules once:
|
|
643
|
+
|
|
644
|
+
```bash
|
|
645
|
+
sudo a2enmod proxy proxy_http headers ssl
|
|
646
|
+
sudo systemctl reload apache2
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
```apache
|
|
650
|
+
<VirtualHost *:443>
|
|
651
|
+
ServerName app.example.com
|
|
652
|
+
|
|
653
|
+
SSLEngine on
|
|
654
|
+
SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
|
|
655
|
+
SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
|
|
656
|
+
|
|
657
|
+
# The service worker must never be served from a stale cache.
|
|
658
|
+
<Files "sw.js">
|
|
659
|
+
Header always set Cache-Control "no-cache, no-store, must-revalidate"
|
|
660
|
+
</Files>
|
|
661
|
+
|
|
662
|
+
# The push API must never be cached by an intermediary.
|
|
663
|
+
<Location "/api/push">
|
|
664
|
+
Header always set Cache-Control "no-store"
|
|
665
|
+
Require all granted
|
|
666
|
+
</Location>
|
|
667
|
+
|
|
668
|
+
ProxyPreserveHost On
|
|
669
|
+
RequestHeader set X-Forwarded-Proto "https"
|
|
670
|
+
|
|
671
|
+
ProxyPass /api/ http://127.0.0.1:3000/api/
|
|
672
|
+
ProxyPassReverse /api/ http://127.0.0.1:3000/api/
|
|
673
|
+
|
|
674
|
+
ProxyPass / http://127.0.0.1:5173/
|
|
675
|
+
ProxyPassReverse / http://127.0.0.1:5173/
|
|
676
|
+
|
|
677
|
+
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
|
|
678
|
+
Header always set X-Content-Type-Options "nosniff"
|
|
679
|
+
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
|
680
|
+
</VirtualHost>
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
## 🩺 Troubleshooting
|
|
686
|
+
|
|
687
|
+
| Symptom | Likely cause | Fix |
|
|
688
|
+
| --- | --- | --- |
|
|
689
|
+
| `Push API is not supported in this browser.` | No service worker, no `PushManager`, or an insecure context. | Serve over HTTPS. |
|
|
690
|
+
| `Notification permission was not granted: denied.` | The user blocked notifications. | Reset the permission in the site settings. |
|
|
691
|
+
| `Failed to upload subscription: 401.` | The session expired between page load and the click. | Re-authenticate, then call `syncWithServer()`. |
|
|
692
|
+
| `TypeError: decodeVapidKey: base64 must be a non-empty string.` | The VAPID key is `undefined`. | Check your environment variable at build time. |
|
|
693
|
+
| `InvalidCharacterError` from `atob`. | You passed the **private** key, or a PEM file. | Use the Base64 URL-safe **public** key. |
|
|
694
|
+
| `AbortError: Registration failed - permission denied`. | `subscribe()` was called without a user gesture. | Wrap it in a `click` handler. |
|
|
695
|
+
| The state is stuck on `'pending'`. | `#upload()` threw. | See [gotchas](#-known-gotchas). |
|
|
696
|
+
| The notification never appears. | The service worker does not call `showNotification()`. | See [the service worker recipe](#recipe-3--service-worker). |
|
|
697
|
+
|
|
698
|
+
---
|
|
699
|
+
|
|
700
|
+
## ⚠️ Known gotchas
|
|
701
|
+
|
|
702
|
+
### 1. A failed upload leaves the state on `'pending'`
|
|
703
|
+
|
|
704
|
+
`subscribe()` sets `status: 'pending'` **before** awaiting the backend. If
|
|
705
|
+
`#upload()` throws, the error propagates and the state is never advanced.
|
|
706
|
+
|
|
707
|
+
```js
|
|
708
|
+
try {
|
|
709
|
+
await push.subscribe();
|
|
710
|
+
} catch (error) {
|
|
711
|
+
// The browser IS subscribed, but the backend does not know yet.
|
|
712
|
+
// Retry the upload without asking for permission again:
|
|
713
|
+
await push.syncWithServer();
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### 2. `isSupported()` throws in Node.js < 21
|
|
718
|
+
|
|
719
|
+
The guard is `'serviceWorker' in navigator`. In a server-side rendering
|
|
720
|
+
environment where `navigator` is not defined at all, this throws a
|
|
721
|
+
`ReferenceError` before the check can return `false`.
|
|
722
|
+
|
|
723
|
+
```js
|
|
724
|
+
// Safe guard for SSR:
|
|
725
|
+
const canUsePush = typeof window !== 'undefined' && TinyPushManager.isSupported();
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### 3. `decodeVapidKey()` caches exactly one key
|
|
729
|
+
|
|
730
|
+
The memoization stores a single `(source, key)` pair. If you rotate keys at
|
|
731
|
+
runtime, the first call after the rotation is a cache miss, which is correct —
|
|
732
|
+
just be aware that it is not an LRU cache.
|
|
733
|
+
|
|
734
|
+
### 4. `unsubscribe()` swallows backend errors
|
|
735
|
+
|
|
736
|
+
This is deliberate. A user must always be able to unsubscribe locally. Schedule
|
|
737
|
+
a reconciliation job to delete orphaned rows.
|
|
738
|
+
|
|
739
|
+
### 5. `syncWithServer()` is a no-op when there is no subscription
|
|
740
|
+
|
|
741
|
+
It returns `null` and does **not** reset the state. If you need a hard reset,
|
|
742
|
+
recreate the manager.
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## 🧪 Testing
|
|
747
|
+
|
|
748
|
+
Inject `fetch` to test without a network:
|
|
749
|
+
|
|
750
|
+
```js
|
|
751
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
752
|
+
import TinyPushManager from 'tiny-essentials/libs/sw/browser/TinyPushManager';
|
|
753
|
+
|
|
754
|
+
describe('TinyPushManager', () => {
|
|
755
|
+
it('throws when the VAPID key is missing', () => {
|
|
756
|
+
expect(
|
|
757
|
+
() => new TinyPushManager({ registration: {}, vapidPublicKey: '' }),
|
|
758
|
+
).toThrow(TypeError);
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
it('uploads the subscription on subscribe()', async () => {
|
|
762
|
+
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 201 });
|
|
763
|
+
const subscription = { toJSON: () => ({ endpoint: 'https://push.test/1' }) };
|
|
764
|
+
|
|
765
|
+
const manager = new TinyPushManager({
|
|
766
|
+
registration: {
|
|
767
|
+
pushManager: { getSubscription: vi.fn().mockResolvedValue(subscription) },
|
|
768
|
+
},
|
|
769
|
+
vapidPublicKey: 'AAAA',
|
|
770
|
+
endpoints: { subscribe: '/api/push' },
|
|
771
|
+
fetch: fetchMock,
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
await manager.syncWithServer();
|
|
775
|
+
|
|
776
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
777
|
+
'/api/push',
|
|
778
|
+
expect.objectContaining({ method: 'POST' }),
|
|
779
|
+
);
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
```
|