tempest-react-sdk 0.38.2 → 0.38.3

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 (291) hide show
  1. package/bin/lib/design/collect.mjs +19 -0
  2. package/bin/lib/design/functions.mjs +5 -0
  3. package/bin/lib/design/index.mjs +8 -3
  4. package/bin/lib/design/index.test.mjs +31 -0
  5. package/bin/lib/design/scan.mjs +34 -2
  6. package/bin/lib/design/scan.test.mjs +78 -0
  7. package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
  8. package/bin/tempest.mjs +90 -6
  9. package/dist/audio/level-meter.cjs.map +1 -1
  10. package/dist/audio/level-meter.js.map +1 -1
  11. package/dist/audio/use-audio-recorder.cjs.map +1 -1
  12. package/dist/audio/use-audio-recorder.js.map +1 -1
  13. package/dist/auth/passkey.cjs.map +1 -1
  14. package/dist/auth/passkey.js.map +1 -1
  15. package/dist/auth/use-passkey.cjs.map +1 -1
  16. package/dist/auth/use-passkey.js.map +1 -1
  17. package/dist/br/BrazilMap.cjs.map +1 -1
  18. package/dist/br/BrazilMap.js.map +1 -1
  19. package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
  20. package/dist/br/BrazilStateCitySelect.js.map +1 -1
  21. package/dist/br/BrazilStateMap.cjs.map +1 -1
  22. package/dist/br/BrazilStateMap.js.map +1 -1
  23. package/dist/br/PixQRCode.cjs.map +1 -1
  24. package/dist/br/PixQRCode.js.map +1 -1
  25. package/dist/br/boleto.cjs.map +1 -1
  26. package/dist/br/boleto.js.map +1 -1
  27. package/dist/br/pix.cjs.map +1 -1
  28. package/dist/br/pix.js.map +1 -1
  29. package/dist/br.d.ts +8 -0
  30. package/dist/capture/media-recorder.cjs.map +1 -1
  31. package/dist/capture/media-recorder.js.map +1 -1
  32. package/dist/capture/use-barcode-scanner.cjs.map +1 -1
  33. package/dist/capture/use-barcode-scanner.js.map +1 -1
  34. package/dist/capture/use-screen-capture.cjs.map +1 -1
  35. package/dist/capture/use-screen-capture.js.map +1 -1
  36. package/dist/capture/use-speech-recognition.cjs.map +1 -1
  37. package/dist/capture/use-speech-recognition.js.map +1 -1
  38. package/dist/capture/use-video-recorder.cjs.map +1 -1
  39. package/dist/capture/use-video-recorder.js.map +1 -1
  40. package/dist/charts/types.cjs.map +1 -1
  41. package/dist/charts/types.js.map +1 -1
  42. package/dist/charts.d.ts +8 -0
  43. package/dist/components/AIChat/AIChat.cjs.map +1 -1
  44. package/dist/components/AIChat/AIChat.js.map +1 -1
  45. package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
  46. package/dist/components/AIChat/AIChatComposer.js.map +1 -1
  47. package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
  48. package/dist/components/AIChat/AIChatTurn.js.map +1 -1
  49. package/dist/components/AppBar/AppBar.cjs.map +1 -1
  50. package/dist/components/AppBar/AppBar.js.map +1 -1
  51. package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
  52. package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
  53. package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
  54. package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
  55. package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
  56. package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
  57. package/dist/components/Button/Button.cjs.map +1 -1
  58. package/dist/components/Button/Button.js.map +1 -1
  59. package/dist/components/Calendar/Calendar.cjs.map +1 -1
  60. package/dist/components/Calendar/Calendar.js.map +1 -1
  61. package/dist/components/Carousel/Carousel.cjs.map +1 -1
  62. package/dist/components/Carousel/Carousel.js.map +1 -1
  63. package/dist/components/Chat/Chat.cjs.map +1 -1
  64. package/dist/components/Chat/Chat.js.map +1 -1
  65. package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
  66. package/dist/components/ChipInput/ChipInput.js.map +1 -1
  67. package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
  68. package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
  69. package/dist/components/Combobox/Combobox.cjs.map +1 -1
  70. package/dist/components/Combobox/Combobox.js.map +1 -1
  71. package/dist/components/Command/Command.cjs.map +1 -1
  72. package/dist/components/Command/Command.js.map +1 -1
  73. package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
  74. package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
  75. package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
  76. package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
  77. package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
  78. package/dist/components/CopyButton/CopyButton.js.map +1 -1
  79. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  80. package/dist/components/DataTable/DataTable.js.map +1 -1
  81. package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
  82. package/dist/components/DataTable/DataTable.module.js.map +1 -1
  83. package/dist/components/DataTable/EditableCell.cjs.map +1 -1
  84. package/dist/components/DataTable/EditableCell.js.map +1 -1
  85. package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
  86. package/dist/components/DatePicker/DatePicker.js.map +1 -1
  87. package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
  88. package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
  89. package/dist/components/Drawer/Drawer.cjs.map +1 -1
  90. package/dist/components/Drawer/Drawer.js.map +1 -1
  91. package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
  92. package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
  93. package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
  94. package/dist/components/Dropzone/Dropzone.js.map +1 -1
  95. package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
  96. package/dist/components/FileUpload/FileUpload.js.map +1 -1
  97. package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
  98. package/dist/components/FilterBar/FilterBar.js.map +1 -1
  99. package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
  100. package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
  101. package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
  102. package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
  103. package/dist/components/Kanban/Kanban.cjs.map +1 -1
  104. package/dist/components/Kanban/Kanban.js.map +1 -1
  105. package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
  106. package/dist/components/Lightbox/Lightbox.js.map +1 -1
  107. package/dist/components/ListTile/ListTile.cjs.map +1 -1
  108. package/dist/components/ListTile/ListTile.js.map +1 -1
  109. package/dist/components/Markdown/Markdown.cjs.map +1 -1
  110. package/dist/components/Markdown/Markdown.js.map +1 -1
  111. package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
  112. package/dist/components/Markdown/markdown-parse.js.map +1 -1
  113. package/dist/components/Menubar/Menubar.cjs.map +1 -1
  114. package/dist/components/Menubar/Menubar.js.map +1 -1
  115. package/dist/components/Modal/Modal.cjs.map +1 -1
  116. package/dist/components/Modal/Modal.js.map +1 -1
  117. package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
  118. package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
  119. package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
  120. package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
  121. package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
  122. package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
  123. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
  124. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
  125. package/dist/components/Page/Page.cjs.map +1 -1
  126. package/dist/components/Page/Page.js.map +1 -1
  127. package/dist/components/Pagination/Pagination.cjs.map +1 -1
  128. package/dist/components/Pagination/Pagination.js.map +1 -1
  129. package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
  130. package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
  131. package/dist/components/PinInput/PinInput.cjs.map +1 -1
  132. package/dist/components/PinInput/PinInput.js.map +1 -1
  133. package/dist/components/Popover/Popover.cjs.map +1 -1
  134. package/dist/components/Popover/Popover.js.map +1 -1
  135. package/dist/components/Progress/Progress.cjs.map +1 -1
  136. package/dist/components/Progress/Progress.js.map +1 -1
  137. package/dist/components/QRCode/qr-encode.cjs.map +1 -1
  138. package/dist/components/QRCode/qr-encode.js.map +1 -1
  139. package/dist/components/Radio/Radio.cjs.map +1 -1
  140. package/dist/components/Radio/Radio.js.map +1 -1
  141. package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
  142. package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
  143. package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
  144. package/dist/components/RatingStars/RatingStars.js.map +1 -1
  145. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  146. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  147. package/dist/components/Resizable/Resizable.cjs.map +1 -1
  148. package/dist/components/Resizable/Resizable.js.map +1 -1
  149. package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
  150. package/dist/components/Scheduler/Scheduler.js.map +1 -1
  151. package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
  152. package/dist/components/Sidebar/Sidebar.js.map +1 -1
  153. package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
  154. package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
  155. package/dist/components/Slider/Slider.cjs.map +1 -1
  156. package/dist/components/Slider/Slider.js.map +1 -1
  157. package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
  158. package/dist/components/Sparkline/Sparkline.js.map +1 -1
  159. package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
  160. package/dist/components/StepperInput/StepperInput.js.map +1 -1
  161. package/dist/components/Table/Table.cjs.map +1 -1
  162. package/dist/components/Table/Table.js.map +1 -1
  163. package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
  164. package/dist/components/TimePicker/TimePicker.js.map +1 -1
  165. package/dist/components/Toast/ToastProvider.cjs.map +1 -1
  166. package/dist/components/Toast/ToastProvider.js.map +1 -1
  167. package/dist/components/Tour/Tour.cjs.map +1 -1
  168. package/dist/components/Tour/Tour.js.map +1 -1
  169. package/dist/components/Transfer/Transfer.cjs.map +1 -1
  170. package/dist/components/Transfer/Transfer.js.map +1 -1
  171. package/dist/components/TreeView/TreeView.cjs.map +1 -1
  172. package/dist/components/TreeView/TreeView.js.map +1 -1
  173. package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
  174. package/dist/components/VirtualList/VirtualList.js.map +1 -1
  175. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
  176. package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
  177. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  178. package/dist/components/Wizard/Wizard.js.map +1 -1
  179. package/dist/editor/RichTextEditor.cjs.map +1 -1
  180. package/dist/editor/RichTextEditor.js.map +1 -1
  181. package/dist/forms/FormField.cjs.map +1 -1
  182. package/dist/forms/FormField.js.map +1 -1
  183. package/dist/geo/TrajectoryMap.cjs.map +1 -1
  184. package/dist/geo/TrajectoryMap.js.map +1 -1
  185. package/dist/geo/estimate.cjs.map +1 -1
  186. package/dist/geo/estimate.js.map +1 -1
  187. package/dist/geo/projection.cjs.map +1 -1
  188. package/dist/geo/projection.js.map +1 -1
  189. package/dist/hooks/use-event-listener.cjs.map +1 -1
  190. package/dist/hooks/use-event-listener.js.map +1 -1
  191. package/dist/hooks/use-local-storage.cjs.map +1 -1
  192. package/dist/hooks/use-local-storage.js.map +1 -1
  193. package/dist/hooks/use-sortable.cjs.map +1 -1
  194. package/dist/hooks/use-sortable.js.map +1 -1
  195. package/dist/http/api-client.cjs.map +1 -1
  196. package/dist/http/api-client.js.map +1 -1
  197. package/dist/http/errors.cjs.map +1 -1
  198. package/dist/http/errors.js.map +1 -1
  199. package/dist/http/resumable-upload.cjs.map +1 -1
  200. package/dist/http/resumable-upload.js.map +1 -1
  201. package/dist/http/upload-with-progress.cjs.map +1 -1
  202. package/dist/http/upload-with-progress.js.map +1 -1
  203. package/dist/i18n/I18nProvider.cjs.map +1 -1
  204. package/dist/i18n/I18nProvider.js.map +1 -1
  205. package/dist/imaging/canvas.cjs.map +1 -1
  206. package/dist/imaging/canvas.js.map +1 -1
  207. package/dist/imaging.d.ts +5 -0
  208. package/dist/logger/logger.cjs.map +1 -1
  209. package/dist/logger/logger.js.map +1 -1
  210. package/dist/oauth/GoogleSignIn.cjs.map +1 -1
  211. package/dist/oauth/GoogleSignIn.js.map +1 -1
  212. package/dist/offline/create-offline-sync.cjs.map +1 -1
  213. package/dist/offline/create-offline-sync.js.map +1 -1
  214. package/dist/perf/cache-size.cjs +1 -1
  215. package/dist/perf/cache-size.cjs.map +1 -1
  216. package/dist/perf/cache-size.js +27 -6
  217. package/dist/perf/cache-size.js.map +1 -1
  218. package/dist/sse/create-event-stream.cjs.map +1 -1
  219. package/dist/sse/create-event-stream.js.map +1 -1
  220. package/dist/styles.css +1 -1
  221. package/dist/sw/background-sync.cjs.map +1 -1
  222. package/dist/sw/background-sync.js.map +1 -1
  223. package/dist/sw/cache.cjs.map +1 -1
  224. package/dist/sw/cache.js.map +1 -1
  225. package/dist/sw.d.ts +12 -0
  226. package/dist/tabular/compact.cjs.map +1 -1
  227. package/dist/tabular/compact.js.map +1 -1
  228. package/dist/tabular/predictor.cjs.map +1 -1
  229. package/dist/tabular/predictor.js.map +1 -1
  230. package/dist/tabular.d.ts +7 -2
  231. package/dist/tempest-react-sdk.d.ts +133 -6
  232. package/dist/theme/ThemeProvider.cjs.map +1 -1
  233. package/dist/theme/ThemeProvider.js.map +1 -1
  234. package/dist/theme/color.cjs.map +1 -1
  235. package/dist/theme/color.js.map +1 -1
  236. package/dist/theme/create-theme.cjs.map +1 -1
  237. package/dist/theme/create-theme.js.map +1 -1
  238. package/dist/theme/data-viz-ramps.cjs +1 -1
  239. package/dist/theme/data-viz-ramps.cjs.map +1 -1
  240. package/dist/theme/data-viz-ramps.js +1 -1
  241. package/dist/theme/data-viz-ramps.js.map +1 -1
  242. package/dist/utils/storage.cjs.map +1 -1
  243. package/dist/utils/storage.js.map +1 -1
  244. package/dist/vision/core/canvas.cjs.map +1 -1
  245. package/dist/vision/core/canvas.js.map +1 -1
  246. package/dist/vision/core/exceptions.cjs.map +1 -1
  247. package/dist/vision/core/exceptions.js.map +1 -1
  248. package/dist/vision/core/graph.cjs.map +1 -1
  249. package/dist/vision/core/graph.js.map +1 -1
  250. package/dist/vision/core/metadata.cjs.map +1 -1
  251. package/dist/vision/core/metadata.js.map +1 -1
  252. package/dist/vision/core/providers.cjs.map +1 -1
  253. package/dist/vision/core/providers.js.map +1 -1
  254. package/dist/vision/core/session.cjs.map +1 -1
  255. package/dist/vision/core/session.js.map +1 -1
  256. package/dist/vision/core/timing.cjs.map +1 -1
  257. package/dist/vision/core/timing.js.map +1 -1
  258. package/dist/vision/io/image.cjs.map +1 -1
  259. package/dist/vision/io/image.js.map +1 -1
  260. package/dist/vision/labels.cjs.map +1 -1
  261. package/dist/vision/labels.js.map +1 -1
  262. package/dist/vision/luminance.cjs.map +1 -1
  263. package/dist/vision/luminance.js.map +1 -1
  264. package/dist/vision/postprocess/classification.cjs.map +1 -1
  265. package/dist/vision/postprocess/classification.js.map +1 -1
  266. package/dist/vision/postprocess/detection.cjs.map +1 -1
  267. package/dist/vision/postprocess/detection.js.map +1 -1
  268. package/dist/vision/postprocess/segmentation.cjs.map +1 -1
  269. package/dist/vision/postprocess/segmentation.js.map +1 -1
  270. package/dist/vision/preprocess/image.cjs.map +1 -1
  271. package/dist/vision/preprocess/image.js.map +1 -1
  272. package/dist/vision/results.cjs.map +1 -1
  273. package/dist/vision/results.js.map +1 -1
  274. package/dist/vision/tasks/base.cjs.map +1 -1
  275. package/dist/vision/tasks/base.js.map +1 -1
  276. package/dist/vision/tasks/classifier.cjs.map +1 -1
  277. package/dist/vision/tasks/classifier.js.map +1 -1
  278. package/dist/vision/tasks/detector.cjs.map +1 -1
  279. package/dist/vision/tasks/detector.js.map +1 -1
  280. package/dist/vision/tasks/segmenter.cjs.map +1 -1
  281. package/dist/vision/tasks/segmenter.js.map +1 -1
  282. package/dist/vision/types.cjs.map +1 -1
  283. package/dist/vision/types.js.map +1 -1
  284. package/dist/vision.d.ts +6 -0
  285. package/dist/vite/tempest-icons.cjs.map +1 -1
  286. package/dist/vite/tempest-icons.js.map +1 -1
  287. package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
  288. package/dist/vite/tempest-pwa-icons.js.map +1 -1
  289. package/dist/ws/create-web-socket.cjs.map +1 -1
  290. package/dist/ws/create-web-socket.js.map +1 -1
  291. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"background-sync.cjs","names":[],"sources":["../../src/sw/background-sync.ts"],"sourcesContent":["/**\n * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)\n * while offline and replay them when connectivity returns. A dependency-free\n * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.\n *\n * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)\n * when available, and also replays opportunistically on the next request as a\n * fallback for browsers without it (e.g. Safari).\n *\n * @example\n * import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n * installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\n\ninterface SyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface PeriodicSyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgSwGlobal {\n registration: { sync?: { register(tag: string): Promise<void> } };\n addEventListener(type: \"sync\", listener: (event: SyncEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: BgFetchEventLike) => void): void;\n addEventListener(type: \"periodicsync\", listener: (event: PeriodicSyncEventLike) => void): void;\n}\n\nfunction getSwScope(): BgSwGlobal {\n return globalThis as unknown as BgSwGlobal;\n}\n\n/** A serialized request stored in the queue. */\ninterface QueuedRequest {\n id?: number;\n url: string;\n method: string;\n headers: [string, string][];\n body: ArrayBuffer | null;\n timestamp: number;\n}\n\n/** Options for {@link installBackgroundSync}. */\nexport interface InstallBackgroundSyncOptions {\n /**\n * Which requests to queue on failure. A `RegExp` against the URL or a\n * predicate. Only non-`GET` requests are ever considered. Default: all\n * non-`GET` requests.\n */\n match?: RegExp | ((url: URL, request: Request) => boolean);\n /** IndexedDB database name, also used as the sync tag. Default `tempest-bg-sync`. */\n queueName?: string;\n /** Drop queued requests older than this (minutes) on replay. Default `1440` (24h). */\n maxRetentionMinutes?: number;\n /**\n * Also drain the queue on the `periodicsync` event carrying this tag,\n * complementing the one-off `sync` replay so long-pending mutations retry\n * even without a fresh navigation. Register the periodic sync from the main\n * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.\n */\n periodicSyncTag?: string;\n}\n\nconst STORE = \"requests\";\n\nfunction openQueueDb(name: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(name, 1);\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n\nasync function enqueue(name: string, entry: QueuedRequest): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).add(entry);\n await txDone(tx);\n db.close();\n}\n\nasync function readAll(name: string): Promise<QueuedRequest[]> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readonly\");\n const all = await new Promise<QueuedRequest[]>((resolve, reject) => {\n const req = tx.objectStore(STORE).getAll();\n req.onsuccess = () => resolve(req.result as QueuedRequest[]);\n req.onerror = () => reject(req.error);\n });\n db.close();\n return all;\n}\n\nasync function remove(name: string, id: number): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(id);\n await txDone(tx);\n db.close();\n}\n\nasync function serializeRequest(request: Request, timestamp: number): Promise<QueuedRequest> {\n const buffer = await request.clone().arrayBuffer();\n return {\n url: request.url,\n method: request.method,\n headers: [...request.headers.entries()],\n body: buffer.byteLength > 0 ? buffer : null,\n timestamp,\n };\n}\n\nfunction deserializeRequest(entry: QueuedRequest): Request {\n return new Request(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: entry.body ?? undefined,\n });\n}\n\n/**\n * Replay every queued request once. Successful (or stale) entries are removed;\n * entries that fail again are kept for the next attempt. Throws if any entry\n * still fails, so a `sync` handler keeps the sync pending.\n */\nasync function replayQueue(name: string, maxRetentionMs: number, now: number): Promise<void> {\n const entries = await readAll(name);\n let pending = 0;\n\n for (const entry of entries) {\n if (entry.id === undefined) continue;\n if (now - entry.timestamp > maxRetentionMs) {\n await remove(name, entry.id);\n continue;\n }\n try {\n const response = await fetch(deserializeRequest(entry));\n if (response.ok || (response.status >= 400 && response.status < 500)) {\n // 2xx = done; 4xx = client error that won't fix itself → drop it.\n await remove(name, entry.id);\n } else {\n pending += 1;\n }\n } catch {\n pending += 1;\n }\n }\n\n if (pending > 0) throw new Error(`background-sync: ${pending} request(s) still pending`);\n}\n\nfunction matches(\n match: InstallBackgroundSyncOptions[\"match\"],\n url: URL,\n request: Request,\n): boolean {\n if (!match) return true;\n return typeof match === \"function\" ? match(url, request) : match.test(url.href);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB and a sync is registered; the original fetch still\n * rejects (so your app can show an offline state), and the request is replayed\n * later when the network returns.\n */\nexport function installBackgroundSync(options: InstallBackgroundSyncOptions = {}): void {\n const sw = getSwScope();\n const {\n match,\n queueName = \"tempest-bg-sync\",\n maxRetentionMinutes = 1440,\n periodicSyncTag = `${options.queueName ?? \"tempest-bg-sync\"}-periodic`,\n } = options;\n const maxRetentionMs = maxRetentionMinutes * 60 * 1000;\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method === \"GET\" || request.method === \"HEAD\") return;\n\n const url = new URL(request.url);\n if (!matches(match, url, request)) return;\n\n event.respondWith(\n fetch(request.clone()).catch(async (error) => {\n const entry = await serializeRequest(request, Date.now());\n await enqueue(queueName, entry);\n try {\n await sw.registration.sync?.register(queueName);\n } catch {\n // Background Sync API unavailable — opportunistic replay covers it.\n }\n throw error;\n }),\n );\n });\n\n sw.addEventListener(\"sync\", (event) => {\n if (event.tag !== queueName) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()));\n });\n\n sw.addEventListener(\"periodicsync\", (event) => {\n if (event.tag !== periodicSyncTag) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n\n // Fallback for browsers without the Background Sync API: whenever a GET\n // succeeds (a sign we're online), drain the queue opportunistically.\n sw.addEventListener(\"fetch\", (event) => {\n if (sw.registration.sync) return; // native sync will handle it\n if (event.request.method !== \"GET\") return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"background-sync.cjs","names":[],"sources":["../../src/sw/background-sync.ts"],"sourcesContent":["/**\n * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)\n * while offline and replay them when connectivity returns. A dependency-free\n * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.\n *\n * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)\n * when available, and also replays opportunistically on the next request as a\n * fallback for browsers without it (e.g. Safari).\n *\n * @example\n * import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n * installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\n\ninterface SyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface PeriodicSyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgSwGlobal {\n registration: { sync?: { register(tag: string): Promise<void> } };\n addEventListener(type: \"sync\", listener: (event: SyncEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: BgFetchEventLike) => void): void;\n addEventListener(type: \"periodicsync\", listener: (event: PeriodicSyncEventLike) => void): void;\n}\n\nfunction getSwScope(): BgSwGlobal {\n return globalThis as unknown as BgSwGlobal;\n}\n\n/** A serialized request stored in the queue. */\ninterface QueuedRequest {\n id?: number;\n url: string;\n method: string;\n headers: [string, string][];\n body: ArrayBuffer | null;\n timestamp: number;\n}\n\n/** Options for {@link installBackgroundSync}. */\nexport interface InstallBackgroundSyncOptions {\n /**\n * Which requests to queue on failure. A `RegExp` against the URL or a\n * predicate. Only non-`GET` requests are ever considered. Default: all\n * non-`GET` requests.\n */\n match?: RegExp | ((url: URL, request: Request) => boolean);\n /** IndexedDB database name, also used as the sync tag. Default `tempest-bg-sync`. */\n queueName?: string;\n /** Drop queued requests older than this (minutes) on replay. Default `1440` (24h). */\n maxRetentionMinutes?: number;\n /**\n * Also drain the queue on the `periodicsync` event carrying this tag,\n * complementing the one-off `sync` replay so long-pending mutations retry\n * even without a fresh navigation. Register the periodic sync from the main\n * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.\n */\n periodicSyncTag?: string;\n}\n\nconst STORE = \"requests\";\n\nfunction openQueueDb(name: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(name, 1);\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n\nasync function enqueue(name: string, entry: QueuedRequest): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).add(entry);\n await txDone(tx);\n db.close();\n}\n\nasync function readAll(name: string): Promise<QueuedRequest[]> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readonly\");\n const all = await new Promise<QueuedRequest[]>((resolve, reject) => {\n const req = tx.objectStore(STORE).getAll();\n req.onsuccess = () => resolve(req.result as QueuedRequest[]);\n req.onerror = () => reject(req.error);\n });\n db.close();\n return all;\n}\n\nasync function remove(name: string, id: number): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(id);\n await txDone(tx);\n db.close();\n}\n\nasync function serializeRequest(request: Request, timestamp: number): Promise<QueuedRequest> {\n const buffer = await request.clone().arrayBuffer();\n return {\n url: request.url,\n method: request.method,\n headers: [...request.headers.entries()],\n body: buffer.byteLength > 0 ? buffer : null,\n timestamp,\n };\n}\n\nfunction deserializeRequest(entry: QueuedRequest): Request {\n return new Request(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: entry.body ?? undefined,\n });\n}\n\n/**\n * Replay every queued request once. Successful (or stale) entries are removed;\n * entries that fail again are kept for the next attempt. Throws if any entry\n * still fails, so a `sync` handler keeps the sync pending.\n */\nasync function replayQueue(name: string, maxRetentionMs: number, now: number): Promise<void> {\n const entries = await readAll(name);\n let pending = 0;\n\n for (const entry of entries) {\n if (entry.id === undefined) continue;\n if (now - entry.timestamp > maxRetentionMs) {\n await remove(name, entry.id);\n continue;\n }\n try {\n const response = await fetch(deserializeRequest(entry));\n if (response.ok || (response.status >= 400 && response.status < 500)) {\n // 2xx = done; 4xx = client error that won't fix itself → drop it.\n await remove(name, entry.id);\n } else {\n pending += 1;\n }\n } catch {\n pending += 1;\n }\n }\n\n if (pending > 0) throw new Error(`background-sync: ${pending} request(s) still pending`);\n}\n\nfunction matches(\n match: InstallBackgroundSyncOptions[\"match\"],\n url: URL,\n request: Request,\n): boolean {\n if (!match) return true;\n return typeof match === \"function\" ? match(url, request) : match.test(url.href);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB and a sync is registered; the original fetch still\n * rejects (so your app can show an offline state), and the request is replayed\n * later when the network returns.\n *\n * @tempest-limits empty-catch — `registration.sync.register` is unavailable on\n * every non-Chromium browser and fails on a revoked background-sync permission.\n * The entry is already durable in IndexedDB at that point, and the queue is also\n * drained opportunistically on the next successful request, so losing the OS-level\n * wakeup degrades *when* the replay happens, not *whether* it happens.\n */\nexport function installBackgroundSync(options: InstallBackgroundSyncOptions = {}): void {\n const sw = getSwScope();\n const {\n match,\n queueName = \"tempest-bg-sync\",\n maxRetentionMinutes = 1440,\n periodicSyncTag = `${options.queueName ?? \"tempest-bg-sync\"}-periodic`,\n } = options;\n const maxRetentionMs = maxRetentionMinutes * 60 * 1000;\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method === \"GET\" || request.method === \"HEAD\") return;\n\n const url = new URL(request.url);\n if (!matches(match, url, request)) return;\n\n event.respondWith(\n fetch(request.clone()).catch(async (error) => {\n const entry = await serializeRequest(request, Date.now());\n await enqueue(queueName, entry);\n try {\n await sw.registration.sync?.register(queueName);\n } catch {\n /* empty */\n }\n throw error;\n }),\n );\n });\n\n sw.addEventListener(\"sync\", (event) => {\n if (event.tag !== queueName) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()));\n });\n\n sw.addEventListener(\"periodicsync\", (event) => {\n if (event.tag !== periodicSyncTag) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n\n // Fallback for browsers without the Background Sync API: whenever a GET\n // succeeds (a sign we're online), drain the queue opportunistically.\n sw.addEventListener(\"fetch\", (event) => {\n if (sw.registration.sync) return; // native sync will handle it\n if (event.request.method !== \"GET\") return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"background-sync.js","names":[],"sources":["../../src/sw/background-sync.ts"],"sourcesContent":["/**\n * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)\n * while offline and replay them when connectivity returns. A dependency-free\n * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.\n *\n * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)\n * when available, and also replays opportunistically on the next request as a\n * fallback for browsers without it (e.g. Safari).\n *\n * @example\n * import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n * installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\n\ninterface SyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface PeriodicSyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgSwGlobal {\n registration: { sync?: { register(tag: string): Promise<void> } };\n addEventListener(type: \"sync\", listener: (event: SyncEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: BgFetchEventLike) => void): void;\n addEventListener(type: \"periodicsync\", listener: (event: PeriodicSyncEventLike) => void): void;\n}\n\nfunction getSwScope(): BgSwGlobal {\n return globalThis as unknown as BgSwGlobal;\n}\n\n/** A serialized request stored in the queue. */\ninterface QueuedRequest {\n id?: number;\n url: string;\n method: string;\n headers: [string, string][];\n body: ArrayBuffer | null;\n timestamp: number;\n}\n\n/** Options for {@link installBackgroundSync}. */\nexport interface InstallBackgroundSyncOptions {\n /**\n * Which requests to queue on failure. A `RegExp` against the URL or a\n * predicate. Only non-`GET` requests are ever considered. Default: all\n * non-`GET` requests.\n */\n match?: RegExp | ((url: URL, request: Request) => boolean);\n /** IndexedDB database name, also used as the sync tag. Default `tempest-bg-sync`. */\n queueName?: string;\n /** Drop queued requests older than this (minutes) on replay. Default `1440` (24h). */\n maxRetentionMinutes?: number;\n /**\n * Also drain the queue on the `periodicsync` event carrying this tag,\n * complementing the one-off `sync` replay so long-pending mutations retry\n * even without a fresh navigation. Register the periodic sync from the main\n * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.\n */\n periodicSyncTag?: string;\n}\n\nconst STORE = \"requests\";\n\nfunction openQueueDb(name: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(name, 1);\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n\nasync function enqueue(name: string, entry: QueuedRequest): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).add(entry);\n await txDone(tx);\n db.close();\n}\n\nasync function readAll(name: string): Promise<QueuedRequest[]> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readonly\");\n const all = await new Promise<QueuedRequest[]>((resolve, reject) => {\n const req = tx.objectStore(STORE).getAll();\n req.onsuccess = () => resolve(req.result as QueuedRequest[]);\n req.onerror = () => reject(req.error);\n });\n db.close();\n return all;\n}\n\nasync function remove(name: string, id: number): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(id);\n await txDone(tx);\n db.close();\n}\n\nasync function serializeRequest(request: Request, timestamp: number): Promise<QueuedRequest> {\n const buffer = await request.clone().arrayBuffer();\n return {\n url: request.url,\n method: request.method,\n headers: [...request.headers.entries()],\n body: buffer.byteLength > 0 ? buffer : null,\n timestamp,\n };\n}\n\nfunction deserializeRequest(entry: QueuedRequest): Request {\n return new Request(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: entry.body ?? undefined,\n });\n}\n\n/**\n * Replay every queued request once. Successful (or stale) entries are removed;\n * entries that fail again are kept for the next attempt. Throws if any entry\n * still fails, so a `sync` handler keeps the sync pending.\n */\nasync function replayQueue(name: string, maxRetentionMs: number, now: number): Promise<void> {\n const entries = await readAll(name);\n let pending = 0;\n\n for (const entry of entries) {\n if (entry.id === undefined) continue;\n if (now - entry.timestamp > maxRetentionMs) {\n await remove(name, entry.id);\n continue;\n }\n try {\n const response = await fetch(deserializeRequest(entry));\n if (response.ok || (response.status >= 400 && response.status < 500)) {\n // 2xx = done; 4xx = client error that won't fix itself → drop it.\n await remove(name, entry.id);\n } else {\n pending += 1;\n }\n } catch {\n pending += 1;\n }\n }\n\n if (pending > 0) throw new Error(`background-sync: ${pending} request(s) still pending`);\n}\n\nfunction matches(\n match: InstallBackgroundSyncOptions[\"match\"],\n url: URL,\n request: Request,\n): boolean {\n if (!match) return true;\n return typeof match === \"function\" ? match(url, request) : match.test(url.href);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB and a sync is registered; the original fetch still\n * rejects (so your app can show an offline state), and the request is replayed\n * later when the network returns.\n */\nexport function installBackgroundSync(options: InstallBackgroundSyncOptions = {}): void {\n const sw = getSwScope();\n const {\n match,\n queueName = \"tempest-bg-sync\",\n maxRetentionMinutes = 1440,\n periodicSyncTag = `${options.queueName ?? \"tempest-bg-sync\"}-periodic`,\n } = options;\n const maxRetentionMs = maxRetentionMinutes * 60 * 1000;\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method === \"GET\" || request.method === \"HEAD\") return;\n\n const url = new URL(request.url);\n if (!matches(match, url, request)) return;\n\n event.respondWith(\n fetch(request.clone()).catch(async (error) => {\n const entry = await serializeRequest(request, Date.now());\n await enqueue(queueName, entry);\n try {\n await sw.registration.sync?.register(queueName);\n } catch {\n // Background Sync API unavailable — opportunistic replay covers it.\n }\n throw error;\n }),\n );\n });\n\n sw.addEventListener(\"sync\", (event) => {\n if (event.tag !== queueName) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()));\n });\n\n sw.addEventListener(\"periodicsync\", (event) => {\n if (event.tag !== periodicSyncTag) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n\n // Fallback for browsers without the Background Sync API: whenever a GET\n // succeeds (a sign we're online), drain the queue opportunistically.\n sw.addEventListener(\"fetch\", (event) => {\n if (sw.registration.sync) return; // native sync will handle it\n if (event.request.method !== \"GET\") return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"background-sync.js","names":[],"sources":["../../src/sw/background-sync.ts"],"sourcesContent":["/**\n * Background-sync helper: queue failed mutating requests (POST/PUT/PATCH/DELETE)\n * while offline and replay them when connectivity returns. A dependency-free\n * take on Workbox's `BackgroundSyncPlugin`, backed by a tiny IndexedDB queue.\n *\n * Import inside your `sw.ts`. Uses the Background Sync API (`registration.sync`)\n * when available, and also replays opportunistically on the next request as a\n * fallback for browsers without it (e.g. Safari).\n *\n * @example\n * import { installBackgroundSync } from \"tempest-react-sdk/sw\";\n *\n * installBackgroundSync({ match: (url) => url.pathname.startsWith(\"/api/\") });\n */\n\ninterface SyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgFetchEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface PeriodicSyncEventLike {\n tag: string;\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface BgSwGlobal {\n registration: { sync?: { register(tag: string): Promise<void> } };\n addEventListener(type: \"sync\", listener: (event: SyncEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: BgFetchEventLike) => void): void;\n addEventListener(type: \"periodicsync\", listener: (event: PeriodicSyncEventLike) => void): void;\n}\n\nfunction getSwScope(): BgSwGlobal {\n return globalThis as unknown as BgSwGlobal;\n}\n\n/** A serialized request stored in the queue. */\ninterface QueuedRequest {\n id?: number;\n url: string;\n method: string;\n headers: [string, string][];\n body: ArrayBuffer | null;\n timestamp: number;\n}\n\n/** Options for {@link installBackgroundSync}. */\nexport interface InstallBackgroundSyncOptions {\n /**\n * Which requests to queue on failure. A `RegExp` against the URL or a\n * predicate. Only non-`GET` requests are ever considered. Default: all\n * non-`GET` requests.\n */\n match?: RegExp | ((url: URL, request: Request) => boolean);\n /** IndexedDB database name, also used as the sync tag. Default `tempest-bg-sync`. */\n queueName?: string;\n /** Drop queued requests older than this (minutes) on replay. Default `1440` (24h). */\n maxRetentionMinutes?: number;\n /**\n * Also drain the queue on the `periodicsync` event carrying this tag,\n * complementing the one-off `sync` replay so long-pending mutations retry\n * even without a fresh navigation. Register the periodic sync from the main\n * thread with {@link registerPeriodicSync}. Default `${queueName}-periodic`.\n */\n periodicSyncTag?: string;\n}\n\nconst STORE = \"requests\";\n\nfunction openQueueDb(name: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const request = indexedDB.open(name, 1);\n request.onupgradeneeded = () => {\n request.result.createObjectStore(STORE, { keyPath: \"id\", autoIncrement: true });\n };\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n}\n\nfunction txDone(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error);\n });\n}\n\nasync function enqueue(name: string, entry: QueuedRequest): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).add(entry);\n await txDone(tx);\n db.close();\n}\n\nasync function readAll(name: string): Promise<QueuedRequest[]> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readonly\");\n const all = await new Promise<QueuedRequest[]>((resolve, reject) => {\n const req = tx.objectStore(STORE).getAll();\n req.onsuccess = () => resolve(req.result as QueuedRequest[]);\n req.onerror = () => reject(req.error);\n });\n db.close();\n return all;\n}\n\nasync function remove(name: string, id: number): Promise<void> {\n const db = await openQueueDb(name);\n const tx = db.transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(id);\n await txDone(tx);\n db.close();\n}\n\nasync function serializeRequest(request: Request, timestamp: number): Promise<QueuedRequest> {\n const buffer = await request.clone().arrayBuffer();\n return {\n url: request.url,\n method: request.method,\n headers: [...request.headers.entries()],\n body: buffer.byteLength > 0 ? buffer : null,\n timestamp,\n };\n}\n\nfunction deserializeRequest(entry: QueuedRequest): Request {\n return new Request(entry.url, {\n method: entry.method,\n headers: entry.headers,\n body: entry.body ?? undefined,\n });\n}\n\n/**\n * Replay every queued request once. Successful (or stale) entries are removed;\n * entries that fail again are kept for the next attempt. Throws if any entry\n * still fails, so a `sync` handler keeps the sync pending.\n */\nasync function replayQueue(name: string, maxRetentionMs: number, now: number): Promise<void> {\n const entries = await readAll(name);\n let pending = 0;\n\n for (const entry of entries) {\n if (entry.id === undefined) continue;\n if (now - entry.timestamp > maxRetentionMs) {\n await remove(name, entry.id);\n continue;\n }\n try {\n const response = await fetch(deserializeRequest(entry));\n if (response.ok || (response.status >= 400 && response.status < 500)) {\n // 2xx = done; 4xx = client error that won't fix itself → drop it.\n await remove(name, entry.id);\n } else {\n pending += 1;\n }\n } catch {\n pending += 1;\n }\n }\n\n if (pending > 0) throw new Error(`background-sync: ${pending} request(s) still pending`);\n}\n\nfunction matches(\n match: InstallBackgroundSyncOptions[\"match\"],\n url: URL,\n request: Request,\n): boolean {\n if (!match) return true;\n return typeof match === \"function\" ? match(url, request) : match.test(url.href);\n}\n\n/**\n * Install the background-sync queue: on a failed mutating request, the request\n * is serialized to IndexedDB and a sync is registered; the original fetch still\n * rejects (so your app can show an offline state), and the request is replayed\n * later when the network returns.\n *\n * @tempest-limits empty-catch — `registration.sync.register` is unavailable on\n * every non-Chromium browser and fails on a revoked background-sync permission.\n * The entry is already durable in IndexedDB at that point, and the queue is also\n * drained opportunistically on the next successful request, so losing the OS-level\n * wakeup degrades *when* the replay happens, not *whether* it happens.\n */\nexport function installBackgroundSync(options: InstallBackgroundSyncOptions = {}): void {\n const sw = getSwScope();\n const {\n match,\n queueName = \"tempest-bg-sync\",\n maxRetentionMinutes = 1440,\n periodicSyncTag = `${options.queueName ?? \"tempest-bg-sync\"}-periodic`,\n } = options;\n const maxRetentionMs = maxRetentionMinutes * 60 * 1000;\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method === \"GET\" || request.method === \"HEAD\") return;\n\n const url = new URL(request.url);\n if (!matches(match, url, request)) return;\n\n event.respondWith(\n fetch(request.clone()).catch(async (error) => {\n const entry = await serializeRequest(request, Date.now());\n await enqueue(queueName, entry);\n try {\n await sw.registration.sync?.register(queueName);\n } catch {\n /* empty */\n }\n throw error;\n }),\n );\n });\n\n sw.addEventListener(\"sync\", (event) => {\n if (event.tag !== queueName) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()));\n });\n\n sw.addEventListener(\"periodicsync\", (event) => {\n if (event.tag !== periodicSyncTag) return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n\n // Fallback for browsers without the Background Sync API: whenever a GET\n // succeeds (a sign we're online), drain the queue opportunistically.\n sw.addEventListener(\"fetch\", (event) => {\n if (sw.registration.sync) return; // native sync will handle it\n if (event.request.method !== \"GET\") return;\n event.waitUntil(replayQueue(queueName, maxRetentionMs, Date.now()).catch(() => undefined));\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache.cjs","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"AAiDA,SAAS,GAA4B,CACjC,OAAO,UACX,CAqDA,IAAI,EAAe,GACb,EAAiB,IAAI,IAG3B,SAAS,EAAU,EAAoB,EAAiC,CACpE,GAAI,CAAC,EAAe,MAAO,GAC3B,IAAM,EAAa,EAAS,QAAQ,IAAI,MAAM,EAG9C,OAFK,GACQ,KAAK,IAAI,EAAI,IAAI,KAAK,CAAU,CAAC,CAAC,QAAQ,GAAK,IAC/C,EAFW,EAG5B,CAGA,eAAe,EAAU,EAAmB,EAAoC,CAC5E,GAAI,CAAC,EAAY,OACjB,IAAM,EAAQ,MAAM,OAAO,KAAK,CAAS,EACnC,EAAO,MAAM,EAAM,KAAK,EAC1B,OAAK,QAAU,GACnB,IAAK,IAAM,KAAO,EAAK,MAAM,EAAG,EAAK,OAAS,CAAU,EACpD,MAAM,EAAM,OAAO,CAAG,CAE9B,CAEA,eAAe,EAAW,EAAkB,EAAwC,CAChF,IAAM,EAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,EACzC,EAAS,MAAM,EAAM,MAAM,CAAO,EACxC,GAAI,GAAU,CAAC,EAAU,EAAQ,EAAM,aAAa,EAAG,OAAO,EAE9D,IAAM,EAAW,MAAM,MAAM,CAAO,EAKpC,OAJI,EAAS,KACT,MAAM,EAAM,IAAI,EAAS,EAAS,MAAM,CAAC,EACzC,MAAM,EAAU,EAAM,UAAW,EAAM,UAAU,GAE9C,CACX,CAEA,eAAe,EAAa,EAAkB,EAAwC,CAClF,IAAM,EAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,EAEzC,GAAW,SAA+B,CAC5C,IAAM,EAAW,MAAM,MAAM,CAAO,EAKpC,OAJI,EAAS,KACT,MAAM,EAAM,IAAI,EAAS,EAAS,MAAM,CAAC,EACzC,MAAM,EAAU,EAAM,UAAW,EAAM,UAAU,GAE9C,CACX,EAAA,CAAG,EAEG,GAAa,EAAM,uBAAyB,GAAK,IACvD,GAAI,CACA,GAAI,EAAY,EAAG,CACf,IAAM,EAAU,IAAI,SAAgB,EAAG,IACnC,eAAiB,EAAW,MAAM,iBAAiB,CAAC,EAAG,CAAS,CACpE,EACA,OAAO,MAAM,QAAQ,KAAK,CAAC,EAAS,CAAO,CAAC,CAChD,CACA,OAAO,MAAM,CACjB,MAAQ,CACJ,IAAM,EAAS,MAAM,EAAM,MAAM,CAAO,EACxC,GAAI,EAAQ,OAAO,EACnB,MAAU,MAAM,wCAAwC,CAC5D,CACJ,CAEA,eAAe,EAAqB,EAAkB,EAAwC,CAC1F,IAAM,EAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,EACzC,EAAS,MAAM,EAAM,MAAM,CAAO,EAElC,EAAU,MAAM,CAAO,CAAC,CACzB,KAAK,KAAO,KACL,EAAS,KACT,MAAM,EAAM,IAAI,EAAS,EAAS,MAAM,CAAC,EACzC,MAAM,EAAU,EAAM,UAAW,EAAM,UAAU,GAE9C,EACV,CAAC,CACD,UAAY,IAAA,EAAS,EAE1B,GAAI,GAAU,CAAC,EAAU,EAAQ,EAAM,aAAa,EAAG,OAAO,EAC9D,IAAM,EAAW,MAAM,EACvB,GAAI,EAAU,OAAO,EACrB,GAAI,EAAQ,OAAO,EACnB,MAAU,MAAM,iDAAiD,CACrE,CAEA,SAAS,EAAS,EAAkB,EAAwC,CACxE,OAAQ,EAAM,SAAd,CACI,IAAK,cACD,OAAO,EAAW,EAAS,CAAK,EACpC,IAAK,gBACD,OAAO,EAAa,EAAS,CAAK,EACtC,IAAK,yBACD,OAAO,EAAqB,EAAS,CAAK,CAClD,CACJ,CAEA,SAAS,EAAa,EAAqB,EAAU,EAA2B,CAC5E,OAAO,OAAO,EAAM,OAAU,WACxB,EAAM,MAAM,EAAK,CAAO,EACxB,EAAM,MAAM,KAAK,EAAI,IAAI,CACnC,CAWA,eAAsB,EAClB,EACA,EACiB,CACjB,IAAM,EAAc,EAAQ,QAAQ,IAAI,OAAO,EAC/C,GAAI,CAAC,EAAa,OAAO,EAEzB,IAAM,EAAQ,sBAAsB,KAAK,EAAY,KAAK,CAAC,EAC3D,GAAI,CAAC,GAAU,EAAM,KAAO,IAAM,EAAM,KAAO,GAC3C,OAAO,IAAI,SAAS,KAAM,CAAE,OAAQ,IAAK,WAAY,uBAAwB,CAAC,EAGlF,IAAM,EAAS,MAAM,EAAS,MAAM,CAAC,CAAC,YAAY,EAC5C,EAAQ,EAAO,WAEjB,EACA,EACJ,GAAI,EAAM,KAAO,GAAI,CAEjB,IAAM,EAAS,OAAO,EAAM,EAAE,EAC9B,EAAQ,KAAK,IAAI,EAAG,EAAQ,CAAM,EAClC,EAAM,EAAQ,CAClB,KACI,GAAQ,OAAO,EAAM,EAAE,EACvB,EAAM,EAAM,KAAO,GAAK,EAAQ,EAAI,KAAK,IAAI,OAAO,EAAM,EAAE,EAAG,EAAQ,CAAC,EAG5E,GAAI,EAAQ,GAAO,GAAS,EACxB,OAAO,IAAI,SAAS,KAAM,CACtB,OAAQ,IACR,WAAY,wBACZ,QAAS,CAAE,gBAAiB,WAAW,GAAQ,CACnD,CAAC,EAGL,IAAM,EAAQ,EAAO,MAAM,EAAO,EAAM,CAAC,EACnC,EAAU,IAAI,QAAQ,EAAS,OAAO,EAK5C,OAJA,EAAQ,IAAI,gBAAiB,SAAS,EAAM,GAAG,EAAI,GAAG,GAAO,EAC7D,EAAQ,IAAI,iBAAkB,OAAO,EAAM,UAAU,CAAC,EACtD,EAAQ,IAAI,gBAAiB,OAAO,EAE7B,IAAI,SAAS,EAAO,CACvB,OAAQ,IACR,WAAY,kBACZ,SACJ,CAAC,CACL,CAYA,SAAgB,EAAoB,EAA8B,CAE9D,EAAA,CAAA,CAAG,iBAAiB,QAAU,GAAU,CACpC,IAAM,EAAU,EAAM,QACtB,GAAI,EAAQ,SAAW,MAAO,OAE9B,IAAM,EAAM,IAAI,IAAI,EAAQ,GAAG,EACzB,EAAQ,EAAO,KAAM,GAAc,EAAa,EAAW,EAAK,CAAO,CAAC,EACzE,KAEL,IAAI,EAAM,eAAiB,EAAQ,QAAQ,IAAI,OAAO,EAAG,CAErD,IAAM,EAAO,IAAI,QAAQ,EAAQ,IAAK,CAClC,QAAS,EAAW,EAAQ,OAAO,EACnC,YAAa,EAAQ,YACrB,KAAM,EAAQ,OAAS,WAAa,cAAgB,EAAQ,IAChE,CAAC,EACD,EAAM,YACF,EAAS,EAAM,CAAK,CAAC,CAAC,KAAM,GAAa,EAAsB,EAAS,CAAQ,CAAC,CACrF,EACA,MACJ,CAEA,EAAM,YAAY,EAAS,EAAS,CAAK,CAAC,CAF1C,CAGJ,CAAC,CACL,CAGA,SAAS,EAAW,EAA2B,CAC3C,IAAM,EAAM,IAAI,QAAQ,CAAO,EAE/B,OADA,EAAI,OAAO,OAAO,EACX,CACX,CAYA,SAAgB,EAAgB,EAAkC,CAAC,EAAS,CACxE,IAAM,EAAK,EAAW,EAChB,CACF,cAAc,0BACd,YAAY,mBACZ,mBAAmB,cACnB,2BAA2B,CAAC,EAC5B,cAAc,GACd,oBAAoB,IACpB,EAEJ,EAAG,iBAAiB,UAAY,GAAU,CACtC,EAAM,WACD,SAAY,CAET,IAAM,EAAY,MAAM,MADD,MAAM,EAAa,CAAE,MAAO,UAAW,CAAC,EAAA,CAC9B,KAAK,EACtC,EAAe,GAAG,EAAU,GAAG,EAAS,UAGxC,MAAM,MADc,OAAO,KAAK,CAAY,EAAA,CAChC,OAAO,EAAS,IAAI,EAChC,IAAK,IAAM,KAAO,EAAS,KACvB,EAAe,IAAI,IAAI,IAAI,EAAK,EAAG,SAAS,MAAM,CAAC,CAAC,QAAQ,EAE5D,GAAa,MAAM,EAAG,YAAY,CAC1C,EAAA,CAAG,CACP,CACJ,CAAC,EAED,EAAG,iBAAiB,WAAa,GAAU,CACvC,EAAM,WACD,SAAY,CACT,IAAM,EAAO,MAAM,OAAO,KAAK,EAC/B,MAAM,QAAQ,IACV,EACK,OAAQ,GAAQ,EAAI,WAAW,GAAG,EAAU,EAAE,GAAK,IAAQ,CAAY,CAAC,CACxE,IAAK,GAAQ,OAAO,OAAO,CAAG,CAAC,CACxC,EACI,GAAqB,EAAG,cAAc,mBACtC,MAAM,EAAG,aAAa,kBAAkB,OAAO,EAEnD,MAAM,EAAG,QAAQ,MAAM,CAC3B,EAAA,CAAG,CACP,CACJ,CAAC,EAED,EAAG,iBAAiB,QAAU,GAAU,CACpC,IAAM,EAAU,EAAM,QACtB,GAAI,EAAQ,SAAW,MAAO,OAE9B,IAAM,EAAM,IAAI,IAAI,EAAQ,GAAG,EAC3B,KAAI,SAAW,EAAG,SAAS,OAG/B,IAAI,EAAQ,OAAS,WAAY,CAC7B,GAAI,EAAyB,KAAM,GAAO,EAAG,KAAK,EAAI,QAAQ,CAAC,EAAG,OAClE,EAAM,aACD,SAAY,CACT,GAAI,CAGA,OADI,MADoB,EAAM,iBAEvB,MAAM,MAAM,CAAO,CAC9B,MAAQ,CAEJ,IAAM,EAAQ,MAAM,MADA,OAAO,KAAK,CAAY,EAAA,CAClB,MAAM,CAAgB,EAChD,GAAI,EAAO,OAAO,EAClB,MAAU,MAAM,iCAAiC,CACrD,CACJ,EAAA,CAAG,CACP,EACA,MACJ,CAGI,EAAe,IAAI,EAAI,QAAQ,GAC/B,EAAM,aACD,SAGU,MADc,MADD,OAAO,KAAK,CAAY,EAAA,CACjB,MAAM,CAAO,GACvB,MAAM,CAAO,EAClC,CAAG,CACP,CAVJ,CAYJ,CAAC,CACL"}
1
+ {"version":3,"file":"cache.cjs","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"cache.js","names":[],"sources":["../../src/sw/cache.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-\n * first, stale-while-revalidate), expiry and the inspect/clear helpers all address\n * the same Cache Storage namespace, and the naming scheme that keeps them from\n * evicting each other is the file's whole contract.\n */\n/**\n * Service-worker caching helpers — a small, dependency-free subset of what\n * Workbox provides: precaching of the build's app shell (so the app launches\n * offline) plus runtime caching strategies for fonts, APIs and images.\n *\n * Import these inside your own `sw.ts`. They run in the service-worker global\n * scope, not the main thread. Pair `installPrecache` with the\n * `tempestPwaManifest()` Vite plugin (from `tempest-react-sdk/vite`), which\n * emits the `precache-manifest.json` this reads at install time.\n *\n * @example\n * /// <reference lib=\"webworker\" />\n * import { installRuntimeCache, installPrecache } from \"tempest-react-sdk/sw\";\n *\n * // Register specific routes FIRST so they win over the precache catch-all.\n * installRuntimeCache([\n * { match: /\\/api\\//, strategy: \"network-first\", cacheName: \"api\", maxAgeSeconds: 300 },\n * ]);\n * installPrecache();\n */\n\n/** Minimal shape of the events we touch, to avoid pulling in `lib.webworker`. */\ninterface ExtendableEventLike {\n waitUntil(promise: Promise<unknown>): void;\n}\n\ninterface FetchEventLike extends ExtendableEventLike {\n request: Request;\n respondWith(response: Response | Promise<Response>): void;\n /** Resolves to the navigation-preload response when it is enabled, else `undefined`. */\n preloadResponse?: Promise<Response | undefined>;\n}\n\n/** Minimal shape of `registration.navigationPreload` (Navigation Preload API). */\ninterface NavigationPreloadManagerLike {\n enable(): Promise<void>;\n disable(): Promise<void>;\n}\n\ninterface CacheSwGlobal {\n addEventListener(type: \"install\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"activate\", listener: (event: ExtendableEventLike) => void): void;\n addEventListener(type: \"fetch\", listener: (event: FetchEventLike) => void): void;\n clients: { claim(): Promise<void> };\n skipWaiting(): Promise<void>;\n location: { origin: string };\n registration?: { navigationPreload?: NavigationPreloadManagerLike };\n}\n\nfunction getSwScope(): CacheSwGlobal {\n return globalThis as unknown as CacheSwGlobal;\n}\n\n/** Caching strategy for a runtime route. Mirrors the common Workbox trio. */\nexport type RuntimeStrategy = \"cache-first\" | \"network-first\" | \"stale-while-revalidate\";\n\n/** A single runtime-caching rule, matched against each `GET` request. */\nexport interface RuntimeRoute {\n /** A `RegExp` tested against the full URL, or a predicate over the parsed URL. */\n match: RegExp | ((url: URL, request: Request) => boolean);\n /** How to resolve a match. */\n strategy: RuntimeStrategy;\n /** Cache bucket name for this route. */\n cacheName: string;\n /** Trim the cache to at most this many entries (FIFO) after each write. */\n maxEntries?: number;\n /** Treat a cached response older than this (seconds) as a miss. */\n maxAgeSeconds?: number;\n /** For `network-first`: fall back to cache after this timeout (seconds). */\n networkTimeoutSeconds?: number;\n /**\n * Serve HTTP `Range` requests (206 Partial Content) by slicing the cached\n * full response. Enable for audio/video so seeking works offline. The full\n * resource is cached once (the `Range` header is stripped before caching).\n */\n rangeRequests?: boolean;\n}\n\n/** Options for {@link installPrecache}. */\nexport interface InstallPrecacheOptions {\n /** URL of the manifest emitted by `tempestPwaManifest()`. Default `/precache-manifest.json`. */\n manifestUrl?: string;\n /** Cache name prefix; the manifest `version` is appended. Default `tempest-precache`. */\n cacheName?: string;\n /** App-shell document served for navigation requests offline. Default `/index.html`. */\n navigateFallback?: string;\n /** Navigation paths that should NOT use the fallback (e.g. `[/^\\/api\\//]`). */\n navigateFallbackDenylist?: RegExp[];\n /** Activate the new worker immediately after precaching. Default `true`. */\n skipWaiting?: boolean;\n /**\n * Enable the Navigation Preload API on `activate`, so the browser fetches\n * the navigation request in parallel with the worker boot and the handler\n * serves `event.preloadResponse` — cutting first-navigation latency after\n * the worker starts. Silently ignored where unsupported. Default `true`.\n */\n navigationPreload?: boolean;\n}\n\ninterface PrecacheManifest {\n version: string;\n urls: string[];\n}\n\nlet precacheName = \"\";\nconst precachedPaths = new Set<string>();\n\n/** Whether a cached response has aged past `maxAgeSeconds` (best-effort, via `Date`). */\nfunction isExpired(response: Response, maxAgeSeconds?: number): boolean {\n if (!maxAgeSeconds) return false;\n const dateHeader = response.headers.get(\"date\");\n if (!dateHeader) return false;\n const age = (Date.now() - new Date(dateHeader).getTime()) / 1000;\n return age > maxAgeSeconds;\n}\n\n/** Drop oldest entries until the cache holds at most `maxEntries`. */\nasync function trimCache(cacheName: string, maxEntries?: number): Promise<void> {\n if (!maxEntries) return;\n const cache = await caches.open(cacheName);\n const keys = await cache.keys();\n if (keys.length <= maxEntries) return;\n for (const key of keys.slice(0, keys.length - maxEntries)) {\n await cache.delete(key);\n }\n}\n\nasync function cacheFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n}\n\nasync function networkFirst(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n\n const network = (async (): Promise<Response> => {\n const response = await fetch(request);\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })();\n\n const timeoutMs = (route.networkTimeoutSeconds ?? 0) * 1000;\n try {\n if (timeoutMs > 0) {\n const timeout = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(\"network-timeout\")), timeoutMs),\n );\n return await Promise.race([network, timeout]);\n }\n return await network;\n } catch {\n const cached = await cache.match(request);\n if (cached) return cached;\n throw new Error(\"network-first: no network and no cache\");\n }\n}\n\nasync function staleWhileRevalidate(request: Request, route: RuntimeRoute): Promise<Response> {\n const cache = await caches.open(route.cacheName);\n const cached = await cache.match(request);\n\n const network = fetch(request)\n .then(async (response) => {\n if (response.ok) {\n await cache.put(request, response.clone());\n await trimCache(route.cacheName, route.maxEntries);\n }\n return response;\n })\n .catch(() => undefined);\n\n if (cached && !isExpired(cached, route.maxAgeSeconds)) return cached;\n const response = await network;\n if (response) return response;\n if (cached) return cached;\n throw new Error(\"stale-while-revalidate: no network and no cache\");\n}\n\nfunction runRoute(request: Request, route: RuntimeRoute): Promise<Response> {\n switch (route.strategy) {\n case \"cache-first\":\n return cacheFirst(request, route);\n case \"network-first\":\n return networkFirst(request, route);\n case \"stale-while-revalidate\":\n return staleWhileRevalidate(request, route);\n }\n}\n\nfunction routeMatches(route: RuntimeRoute, url: URL, request: Request): boolean {\n return typeof route.match === \"function\"\n ? route.match(url, request)\n : route.match.test(url.href);\n}\n\n/**\n * Build a `206 Partial Content` response from a full one for an HTTP `Range`\n * request. Supports `bytes=start-end`, open-ended `bytes=start-` and suffix\n * `bytes=-suffixLength`. Returns the original response when there is no usable\n * `Range` header, or a `416` when the range is unsatisfiable.\n *\n * @param request The incoming request (its `Range` header drives the slice).\n * @param response The full (200) response to slice.\n */\nexport async function createPartialResponse(\n request: Request,\n response: Response,\n): Promise<Response> {\n const rangeHeader = request.headers.get(\"range\");\n if (!rangeHeader) return response;\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match || (match[1] === \"\" && match[2] === \"\")) {\n return new Response(null, { status: 416, statusText: \"Range Not Satisfiable\" });\n }\n\n const buffer = await response.clone().arrayBuffer();\n const total = buffer.byteLength;\n\n let start: number;\n let end: number;\n if (match[1] === \"\") {\n // Suffix range: last N bytes.\n const suffix = Number(match[2]);\n start = Math.max(0, total - suffix);\n end = total - 1;\n } else {\n start = Number(match[1]);\n end = match[2] === \"\" ? total - 1 : Math.min(Number(match[2]), total - 1);\n }\n\n if (start > end || start >= total) {\n return new Response(null, {\n status: 416,\n statusText: \"Range Not Satisfiable\",\n headers: { \"Content-Range\": `bytes */${total}` },\n });\n }\n\n const slice = buffer.slice(start, end + 1);\n const headers = new Headers(response.headers);\n headers.set(\"Content-Range\", `bytes ${start}-${end}/${total}`);\n headers.set(\"Content-Length\", String(slice.byteLength));\n headers.set(\"Accept-Ranges\", \"bytes\");\n\n return new Response(slice, {\n status: 206,\n statusText: \"Partial Content\",\n headers,\n });\n}\n\n/**\n * Install a `fetch` handler that resolves matching `GET` requests with the\n * given runtime strategies. Non-matching requests are left untouched (no\n * `respondWith`), so a later {@link installPrecache} can handle them.\n *\n * Register this BEFORE `installPrecache` so specific routes win over the\n * precache catch-all.\n *\n * @param routes Ordered rules; the first whose `match` passes handles the request.\n */\nexport function installRuntimeCache(routes: RuntimeRoute[]): void {\n const sw = getSwScope();\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n const route = routes.find((candidate) => routeMatches(candidate, url, request));\n if (!route) return;\n\n if (route.rangeRequests && request.headers.has(\"range\")) {\n // Cache/serve the FULL resource once (no Range), then slice it.\n const full = new Request(request.url, {\n headers: stripRange(request.headers),\n credentials: request.credentials,\n mode: request.mode === \"navigate\" ? \"same-origin\" : request.mode,\n });\n event.respondWith(\n runRoute(full, route).then((response) => createPartialResponse(request, response)),\n );\n return;\n }\n\n event.respondWith(runRoute(request, route));\n });\n}\n\n/** Copy headers without the `Range` header (so the cached entry is the full file). */\nfunction stripRange(headers: Headers): Headers {\n const out = new Headers(headers);\n out.delete(\"range\");\n return out;\n}\n\n/**\n * Precache the app shell at `install` and serve it offline:\n * - reads `precache-manifest.json` (emitted by `tempestPwaManifest()`),\n * - caches every listed URL under a versioned cache,\n * - on `activate`, deletes stale precache versions and claims open clients,\n * - on `fetch`, serves precached assets cache-first and falls back to the\n * `navigateFallback` document for offline navigations (SPA routing).\n *\n * Same-origin only. Register this LAST, after any {@link installRuntimeCache}.\n */\nexport function installPrecache(options: InstallPrecacheOptions = {}): void {\n const sw = getSwScope();\n const {\n manifestUrl = \"/precache-manifest.json\",\n cacheName = \"tempest-precache\",\n navigateFallback = \"/index.html\",\n navigateFallbackDenylist = [],\n skipWaiting = true,\n navigationPreload = true,\n } = options;\n\n sw.addEventListener(\"install\", (event) => {\n event.waitUntil(\n (async () => {\n const response = await fetch(manifestUrl, { cache: \"no-cache\" });\n const manifest = (await response.json()) as PrecacheManifest;\n precacheName = `${cacheName}-${manifest.version}`;\n\n const cache = await caches.open(precacheName);\n await cache.addAll(manifest.urls);\n for (const url of manifest.urls) {\n precachedPaths.add(new URL(url, sw.location.origin).pathname);\n }\n if (skipWaiting) await sw.skipWaiting();\n })(),\n );\n });\n\n sw.addEventListener(\"activate\", (event) => {\n event.waitUntil(\n (async () => {\n const keys = await caches.keys();\n await Promise.all(\n keys\n .filter((key) => key.startsWith(`${cacheName}-`) && key !== precacheName)\n .map((key) => caches.delete(key)),\n );\n if (navigationPreload && sw.registration?.navigationPreload) {\n await sw.registration.navigationPreload.enable();\n }\n await sw.clients.claim();\n })(),\n );\n });\n\n sw.addEventListener(\"fetch\", (event) => {\n const request = event.request;\n if (request.method !== \"GET\") return;\n\n const url = new URL(request.url);\n if (url.origin !== sw.location.origin) return;\n\n // SPA navigations: serve the cached app shell when offline.\n if (request.mode === \"navigate\") {\n if (navigateFallbackDenylist.some((re) => re.test(url.pathname))) return;\n event.respondWith(\n (async () => {\n try {\n const preloaded = await event.preloadResponse;\n if (preloaded) return preloaded;\n return await fetch(request);\n } catch {\n const cache = await caches.open(precacheName);\n const shell = await cache.match(navigateFallback);\n if (shell) return shell;\n throw new Error(\"offline and no cached app shell\");\n }\n })(),\n );\n return;\n }\n\n // Precached static assets: cache-first.\n if (precachedPaths.has(url.pathname)) {\n event.respondWith(\n (async () => {\n const cache = await caches.open(precacheName);\n const cached = await cache.match(request);\n return cached ?? fetch(request);\n })(),\n );\n }\n });\n}\n"],"mappings":";AAuDA,SAAS,IAA4B;CACjC,OAAO;AACX;AAqDA,IAAI,IAAe,IACb,oBAAiB,IAAI,IAAY;AAGvC,SAAS,EAAU,GAAoB,GAAiC;CACpE,IAAI,CAAC,GAAe,OAAO;CAC3B,IAAM,IAAa,EAAS,QAAQ,IAAI,MAAM;CAG9C,OAFK,KACQ,KAAK,IAAI,IAAI,IAAI,KAAK,CAAU,CAAC,CAAC,QAAQ,KAAK,MAC/C,IAFW;AAG5B;AAGA,eAAe,EAAU,GAAmB,GAAoC;CAC5E,IAAI,CAAC,GAAY;CACjB,IAAM,IAAQ,MAAM,OAAO,KAAK,CAAS,GACnC,IAAO,MAAM,EAAM,KAAK;CAC1B,QAAK,UAAU,IACnB,KAAK,IAAM,KAAO,EAAK,MAAM,GAAG,EAAK,SAAS,CAAU,GACpD,MAAM,EAAM,OAAO,CAAG;AAE9B;AAEA,eAAe,EAAW,GAAkB,GAAwC;CAChF,IAAM,IAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,GACzC,IAAS,MAAM,EAAM,MAAM,CAAO;CACxC,IAAI,KAAU,CAAC,EAAU,GAAQ,EAAM,aAAa,GAAG,OAAO;CAE9D,IAAM,IAAW,MAAM,MAAM,CAAO;CAKpC,OAJI,EAAS,OACT,MAAM,EAAM,IAAI,GAAS,EAAS,MAAM,CAAC,GACzC,MAAM,EAAU,EAAM,WAAW,EAAM,UAAU,IAE9C;AACX;AAEA,eAAe,EAAa,GAAkB,GAAwC;CAClF,IAAM,IAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,GAEzC,KAAW,YAA+B;EAC5C,IAAM,IAAW,MAAM,MAAM,CAAO;EAKpC,OAJI,EAAS,OACT,MAAM,EAAM,IAAI,GAAS,EAAS,MAAM,CAAC,GACzC,MAAM,EAAU,EAAM,WAAW,EAAM,UAAU,IAE9C;CACX,EAAA,CAAG,GAEG,KAAa,EAAM,yBAAyB,KAAK;CACvD,IAAI;EACA,IAAI,IAAY,GAAG;GACf,IAAM,IAAU,IAAI,SAAgB,GAAG,MACnC,iBAAiB,EAAO,gBAAI,MAAM,iBAAiB,CAAC,GAAG,CAAS,CACpE;GACA,OAAO,MAAM,QAAQ,KAAK,CAAC,GAAS,CAAO,CAAC;EAChD;EACA,OAAO,MAAM;CACjB,QAAQ;EACJ,IAAM,IAAS,MAAM,EAAM,MAAM,CAAO;EACxC,IAAI,GAAQ,OAAO;EACnB,MAAU,MAAM,wCAAwC;CAC5D;AACJ;AAEA,eAAe,EAAqB,GAAkB,GAAwC;CAC1F,IAAM,IAAQ,MAAM,OAAO,KAAK,EAAM,SAAS,GACzC,IAAS,MAAM,EAAM,MAAM,CAAO,GAElC,IAAU,MAAM,CAAO,CAAC,CACzB,KAAK,OAAO,OACL,EAAS,OACT,MAAM,EAAM,IAAI,GAAS,EAAS,MAAM,CAAC,GACzC,MAAM,EAAU,EAAM,WAAW,EAAM,UAAU,IAE9C,EACV,CAAC,CACD,YAAY,KAAA,CAAS;CAE1B,IAAI,KAAU,CAAC,EAAU,GAAQ,EAAM,aAAa,GAAG,OAAO;CAC9D,IAAM,IAAW,MAAM;CACvB,IAAI,GAAU,OAAO;CACrB,IAAI,GAAQ,OAAO;CACnB,MAAU,MAAM,iDAAiD;AACrE;AAEA,SAAS,EAAS,GAAkB,GAAwC;CACxE,QAAQ,EAAM,UAAd;EACI,KAAK,eACD,OAAO,EAAW,GAAS,CAAK;EACpC,KAAK,iBACD,OAAO,EAAa,GAAS,CAAK;EACtC,KAAK,0BACD,OAAO,EAAqB,GAAS,CAAK;CAClD;AACJ;AAEA,SAAS,EAAa,GAAqB,GAAU,GAA2B;CAC5E,OAAO,OAAO,EAAM,SAAU,aACxB,EAAM,MAAM,GAAK,CAAO,IACxB,EAAM,MAAM,KAAK,EAAI,IAAI;AACnC;AAWA,eAAsB,EAClB,GACA,GACiB;CACjB,IAAM,IAAc,EAAQ,QAAQ,IAAI,OAAO;CAC/C,IAAI,CAAC,GAAa,OAAO;CAEzB,IAAM,IAAQ,sBAAsB,KAAK,EAAY,KAAK,CAAC;CAC3D,IAAI,CAAC,KAAU,EAAM,OAAO,MAAM,EAAM,OAAO,IAC3C,OAAO,IAAI,SAAS,MAAM;EAAE,QAAQ;EAAK,YAAY;CAAwB,CAAC;CAGlF,IAAM,IAAS,MAAM,EAAS,MAAM,CAAC,CAAC,YAAY,GAC5C,IAAQ,EAAO,YAEjB,GACA;CACJ,IAAI,EAAM,OAAO,IAAI;EAEjB,IAAM,IAAS,OAAO,EAAM,EAAE;EAE9B,AADA,IAAQ,KAAK,IAAI,GAAG,IAAQ,CAAM,GAClC,IAAM,IAAQ;CAClB,OAEI,AADA,IAAQ,OAAO,EAAM,EAAE,GACvB,IAAM,EAAM,OAAO,KAAK,IAAQ,IAAI,KAAK,IAAI,OAAO,EAAM,EAAE,GAAG,IAAQ,CAAC;CAG5E,IAAI,IAAQ,KAAO,KAAS,GACxB,OAAO,IAAI,SAAS,MAAM;EACtB,QAAQ;EACR,YAAY;EACZ,SAAS,EAAE,iBAAiB,WAAW,IAAQ;CACnD,CAAC;CAGL,IAAM,IAAQ,EAAO,MAAM,GAAO,IAAM,CAAC,GACnC,IAAU,IAAI,QAAQ,EAAS,OAAO;CAK5C,OAJA,EAAQ,IAAI,iBAAiB,SAAS,EAAM,GAAG,EAAI,GAAG,GAAO,GAC7D,EAAQ,IAAI,kBAAkB,OAAO,EAAM,UAAU,CAAC,GACtD,EAAQ,IAAI,iBAAiB,OAAO,GAE7B,IAAI,SAAS,GAAO;EACvB,QAAQ;EACR,YAAY;EACZ;CACJ,CAAC;AACL;AAYA,SAAgB,EAAoB,GAA8B;CAE9D,EAAA,CAAA,CAAG,iBAAiB,UAAU,MAAU;EACpC,IAAM,IAAU,EAAM;EACtB,IAAI,EAAQ,WAAW,OAAO;EAE9B,IAAM,IAAM,IAAI,IAAI,EAAQ,GAAG,GACzB,IAAQ,EAAO,MAAM,MAAc,EAAa,GAAW,GAAK,CAAO,CAAC;EACzE,OAEL;OAAI,EAAM,iBAAiB,EAAQ,QAAQ,IAAI,OAAO,GAAG;IAErD,IAAM,IAAO,IAAI,QAAQ,EAAQ,KAAK;KAClC,SAAS,EAAW,EAAQ,OAAO;KACnC,aAAa,EAAQ;KACrB,MAAM,EAAQ,SAAS,aAAa,gBAAgB,EAAQ;IAChE,CAAC;IACD,EAAM,YACF,EAAS,GAAM,CAAK,CAAC,CAAC,MAAM,MAAa,EAAsB,GAAS,CAAQ,CAAC,CACrF;IACA;GACJ;GAEA,EAAM,YAAY,EAAS,GAAS,CAAK,CAAC;EAF1C;CAGJ,CAAC;AACL;AAGA,SAAS,EAAW,GAA2B;CAC3C,IAAM,IAAM,IAAI,QAAQ,CAAO;CAE/B,OADA,EAAI,OAAO,OAAO,GACX;AACX;AAYA,SAAgB,EAAgB,IAAkC,CAAC,GAAS;CACxE,IAAM,IAAK,EAAW,GAChB,EACF,iBAAc,2BACd,eAAY,oBACZ,sBAAmB,eACnB,8BAA2B,CAAC,GAC5B,iBAAc,IACd,uBAAoB,OACpB;CAoCJ,AAlCA,EAAG,iBAAiB,YAAY,MAAU;EACtC,EAAM,WACD,YAAY;GAET,IAAM,IAAY,OAAM,MADD,MAAM,GAAa,EAAE,OAAO,WAAW,CAAC,EAAA,CAC9B,KAAK;GAItC,AAHA,IAAe,GAAG,EAAU,GAAG,EAAS,WAGxC,OAAM,MADc,OAAO,KAAK,CAAY,EAAA,CAChC,OAAO,EAAS,IAAI;GAChC,KAAK,IAAM,KAAO,EAAS,MACvB,EAAe,IAAI,IAAI,IAAI,GAAK,EAAG,SAAS,MAAM,CAAC,CAAC,QAAQ;GAEhE,AAAI,KAAa,MAAM,EAAG,YAAY;EAC1C,EAAA,CAAG,CACP;CACJ,CAAC,GAED,EAAG,iBAAiB,aAAa,MAAU;EACvC,EAAM,WACD,YAAY;GACT,IAAM,IAAO,MAAM,OAAO,KAAK;GAS/B,AARA,MAAM,QAAQ,IACV,EACK,QAAQ,MAAQ,EAAI,WAAW,GAAG,EAAU,EAAE,KAAK,MAAQ,CAAY,CAAC,CACxE,KAAK,MAAQ,OAAO,OAAO,CAAG,CAAC,CACxC,GACI,KAAqB,EAAG,cAAc,qBACtC,MAAM,EAAG,aAAa,kBAAkB,OAAO,GAEnD,MAAM,EAAG,QAAQ,MAAM;EAC3B,EAAA,CAAG,CACP;CACJ,CAAC,GAED,EAAG,iBAAiB,UAAU,MAAU;EACpC,IAAM,IAAU,EAAM;EACtB,IAAI,EAAQ,WAAW,OAAO;EAE9B,IAAM,IAAM,IAAI,IAAI,EAAQ,GAAG;EAC3B,MAAI,WAAW,EAAG,SAAS,QAG/B;OAAI,EAAQ,SAAS,YAAY;IAC7B,IAAI,EAAyB,MAAM,MAAO,EAAG,KAAK,EAAI,QAAQ,CAAC,GAAG;IAClE,EAAM,aACD,YAAY;KACT,IAAI;MAGA,OADI,MADoB,EAAM,mBAEvB,MAAM,MAAM,CAAO;KAC9B,QAAQ;MAEJ,IAAM,IAAQ,OAAM,MADA,OAAO,KAAK,CAAY,EAAA,CAClB,MAAM,CAAgB;MAChD,IAAI,GAAO,OAAO;MAClB,MAAU,MAAM,iCAAiC;KACrD;IACJ,EAAA,CAAG,CACP;IACA;GACJ;GAGA,AAAI,EAAe,IAAI,EAAI,QAAQ,KAC/B,EAAM,aACD,YAGU,OADc,MADD,OAAO,KAAK,CAAY,EAAA,CACjB,MAAM,CAAO,KACvB,MAAM,CAAO,EAClC,CAAG,CACP;EAVJ;CAYJ,CAAC;AACL"}
package/dist/sw.d.ts CHANGED
@@ -63,6 +63,12 @@ export declare function inspectCaches(options?: {
63
63
  * is serialized to IndexedDB and a sync is registered; the original fetch still
64
64
  * rejects (so your app can show an offline state), and the request is replayed
65
65
  * later when the network returns.
66
+ *
67
+ * @tempest-limits empty-catch — `registration.sync.register` is unavailable on
68
+ * every non-Chromium browser and fails on a revoked background-sync permission.
69
+ * The entry is already durable in IndexedDB at that point, and the queue is also
70
+ * drained opportunistically on the next successful request, so losing the OS-level
71
+ * wakeup degrades *when* the replay happens, not *whether* it happens.
66
72
  */
67
73
  export declare function installBackgroundSync(options?: InstallBackgroundSyncOptions): void;
68
74
 
@@ -319,6 +325,12 @@ export declare interface RuntimeRoute {
319
325
  rangeRequests?: boolean;
320
326
  }
321
327
 
328
+ /**
329
+ * @tempest-limits file-lines — precache, runtime strategies (cache-first, network-
330
+ * first, stale-while-revalidate), expiry and the inspect/clear helpers all address
331
+ * the same Cache Storage namespace, and the naming scheme that keeps them from
332
+ * evicting each other is the file's whole contract.
333
+ */
322
334
  /**
323
335
  * Service-worker caching helpers — a small, dependency-free subset of what
324
336
  * Workbox provides: precaching of the build's app shell (so the app launches
@@ -1 +1 @@
1
- {"version":3,"file":"compact.cjs","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"compact.cjs","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"oCAkCA,IAAM,EAAQ,OAGD,EAA2B,EA4D3B,EAAb,MAAa,CAAiB,CAEL,OACA,OAED,KAJpB,YACI,EACA,EAEA,EACF,CAJmB,KAAA,OAAA,EACA,KAAA,OAAA,EAED,KAAA,KAAA,CACjB,CAYH,aAAa,OACT,EACA,EACyB,CACzB,IAAM,EAAS,MAAM,EAAS,EAAQ,CAAW,EAC3C,EAAQ,IAAI,WAAW,CAAM,EAEnC,GAAI,OAAO,aAAa,GAAG,EAAM,SAAS,EAAG,CAAC,CAAC,IAAM,EACjD,MAAM,IAAI,EAAA,mBACN,6DACQ,EAAM,uDAClB,EAIJ,IAAM,EAAe,IADJ,SAAS,CACL,CAAA,CAAK,UAAU,EAAG,EAAI,EACrC,EAAS,KAAK,MAChB,IAAI,YAAY,CAAC,CAAC,OAAO,EAAM,SAAS,EAAG,EAAI,CAAY,CAAC,CAChE,EAEA,GAAI,EAAO,eAAA,EACP,MAAM,IAAI,EAAA,mBACN,iCAAiC,EAAO,eAAe,2EAG3D,EAGJ,IAAM,EAAoD,CAAC,EACvD,EAAS,EAAI,EACjB,IAAK,IAAM,KAAW,EAAO,SACzB,EAAO,EAAQ,MACX,EAAQ,QAAU,UACZ,IAAI,aAAa,EAAQ,EAAQ,EAAQ,MAAM,EAC/C,IAAI,WAAW,EAAQ,EAAQ,EAAQ,MAAM,EACvD,GAAU,EAAQ,OAAS,EAG/B,OAAO,IAAI,EAAiB,EAAQ,EAAQ,CACxC,KAAM,EAAO,KACb,QAAS,EAAO,QAChB,YAAa,EAAO,WACpB,aAAc,EAAO,cACrB,SAAU,EAAO,SAAW,EAC5B,aAAc,EAAO,OAAS,iBAC9B,UAAW,EAAO,SACtB,CAAC,CACL,CAYA,MAAM,QAAQ,EAAyD,CACnE,IAAM,EAAQ,EAAa,EAAM,KAAK,OAAO,UAAU,EACjD,EAAU,YAAY,IAAI,EAE1B,EAAqB,CAAC,EAC5B,IAAK,IAAM,KAAO,EAAM,CACpB,IAAM,EAAW,KAAK,WAAW,EAAK,CAAK,EAC3C,EAAO,KACH,KAAK,OAAO,OAAS,SACf,KAAK,aAAa,CAAQ,EAC1B,KAAK,WAAW,CAAQ,CAClC,CACJ,CAEA,GAAM,CAAE,SAAQ,iBAAkB,KAAK,OAAO,CAAM,EACpD,MAAO,CACH,SACA,gBACA,QAAS,EAAK,OACd,GAAI,YAAY,IAAI,EAAI,CAC5B,CACJ,CAGA,MAAM,SAAyB,CAE/B,CASA,WAAmB,EAAiB,EAAyB,CACzD,IAAM,EAAa,KAAK,OAAO,WAC/B,GAAI,IAAe,KAAM,OAAO,EAChC,IAAM,EAAa,MAAc,CAAK,EACtC,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,GAAS,EACxC,EAAO,IACD,EAAI,GAAqB,EAAW,OAAO,IAC5C,EAAW,MAAM,GAE1B,OAAO,CACX,CAQA,aAAqB,EAAkC,CACnD,IAAM,EAAO,KAAK,OAAO,KACnB,EAAY,KAAK,OAAO,UACxB,EAAW,KAAK,OAAO,WACvB,EAAU,KAAK,OAAO,UAEtB,EAAa,MAAc,CAAO,EACxC,IAAK,IAAI,EAAS,EAAG,EAAS,EAAS,GAAU,EAAG,CAChD,IAAI,EAAQ,EAAU,GAChB,EAAO,EAAS,EACtB,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAU,GAAS,EAC3C,GAAU,EAAK,EAAO,GAAqB,EAAI,GAEnD,EAAO,GAAU,CACrB,CACA,OAAO,CACX,CAmBA,WAAmB,EAAkC,CACjD,IAAM,EAAU,KAAK,OAAO,aACtB,EAAY,KAAK,OAAO,eACxB,EAAO,KAAK,OAAO,UACnB,EAAQ,KAAK,OAAO,WACpB,EAAO,KAAK,OAAO,WACnB,EAAU,KAAK,OAAO,YACtB,EAAU,KAAK,OAAO,UACtB,EAAQ,EAAQ,OAAS,EAEzB,EAAa,MAAc,CAAO,CAAC,CAAC,KAAK,CAAC,EAChD,IAAK,IAAI,EAAO,EAAG,EAAO,EAAO,GAAQ,EAAG,CACxC,IAAI,EAAO,EAAQ,GACf,EAAS,EAAQ,GACrB,KAAO,GAAU,GACb,EACI,KAAK,OAAO,EAAI,EAAiB,GAAM,EAAU,GAC1C,EAAK,GACL,EAAM,GACjB,EAAS,EAAQ,GAErB,IAAM,GAAQ,GAAK,GAAU,EAC7B,IAAK,IAAI,EAAS,EAAG,EAAS,EAAS,GAAU,EAC7C,EAAO,IAAW,EAAK,EAAO,EAEtC,CACA,IAAK,IAAI,EAAS,EAAG,EAAS,EAAS,GAAU,EAC7C,EAAO,GAAW,EAAO,GAAqB,EAElD,OAAO,CACX,CAaA,OAAe,EAGb,CACE,IAAM,EAAO,KAAK,OAAO,KACnB,EAAU,KAAK,OAAO,QAE5B,GAAI,IAAS,WACT,MAAO,CAAE,OAAQ,EAAO,IAAK,GAAQ,EAAI,EAAY,EAAG,cAAe,CAAC,CAAE,EAG9E,IAAM,EAAgB,EAAO,IAAK,GAAQ,CACtC,GAAI,IAAS,UAAW,CACpB,IAAM,EAAW,GAAK,EAAI,KAAK,IAAI,CAAE,EAAI,EAAa,GACtD,MAAO,CAAC,EAAI,EAAU,CAAQ,CAClC,CACA,GAAI,IAAS,UAAW,CACpB,IAAM,EAAU,KAAK,IAAI,GAAG,CAAG,EACzB,EAAgB,EAAI,IAAK,GAAU,KAAK,IAAI,EAAQ,CAAO,CAAC,EAC5D,EAAQ,EAAc,QAAQ,EAAK,IAAU,EAAM,EAAO,CAAC,EACjE,OAAO,EAAc,IAAK,GAAU,EAAQ,CAAK,CACrD,CACA,IAAM,EAAQ,EAAI,QAAQ,EAAK,IAAU,EAAM,EAAO,CAAC,EACvD,OAAO,IAAU,EAAI,CAAC,GAAG,CAAG,EAAI,EAAI,IAAK,GAAU,EAAQ,CAAK,CACpE,CAAC,EAEK,EAAU,KAAK,OAAO,aAAe,MAW3C,MAAO,CAAE,OAVM,EAAc,IAAK,GAAQ,CACtC,IAAI,EAAO,EACX,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAI,OAAQ,GAAS,EACxC,EAAI,GAAqB,EAAI,KAAkB,EAAO,GAE/D,IAAM,EAAQ,EAAQ,GAEtB,OADI,IAAU,IAAA,GAAkB,EACzB,EAAU,OAAO,CAAK,EAAI,CACrC,CAES,EAAQ,eAAc,CACnC,CACJ,EAUA,eAAe,EACX,EACA,EACoB,CACpB,GAAI,OAAO,GAAW,SAAU,CAC5B,IAAI,EACJ,GAAI,CACA,EAAW,MAAM,MAAM,EAAQ,CAAW,CAC9C,OAAS,EAAO,CACZ,MAAM,IAAI,EAAA,gBAAgB,iCAAiC,IAAU,CACjE,MAAO,CACX,CAAC,CACL,CACA,GAAI,CAAC,EAAS,GACV,MAAM,IAAI,EAAA,gBACN,iCAAiC,EAAS,OAAO,GAAG,EAAS,YACjE,EAEJ,OAAO,MAAM,EAAS,YAAY,CACtC,CAOA,OANI,aAAkB,WACX,EAAO,OAAO,MACjB,EAAO,WACP,EAAO,WAAa,EAAO,UAC/B,EAEG,CACX,CAUA,SAAS,EAAa,EAA6B,EAA0B,CACzE,GAAI,CAAC,MAAM,QAAQ,CAAI,GAAK,EAAK,SAAW,EACxC,MAAM,IAAI,EAAA,kBAAkB,2DAA2D,EAE3F,IAAM,EAAQ,EAAK,EAAE,EAAE,QAAU,EAC3B,EAAS,EAAK,UAAW,GAAQ,EAAI,SAAW,CAAK,EAC3D,GAAI,IAAW,GACX,MAAM,IAAI,EAAA,kBACN,0CAA0C,EAAO,OAC1C,EAAK,EAAO,EAAE,OAAO,oBAAoB,EAAM,EAC1D,EAEJ,GAAI,IAAU,EACV,MAAM,IAAI,EAAA,kBACN,qBAAqB,EAAS,yBAAyB,EAAM,EACjE,EAEJ,OAAO,CACX"}
@@ -1 +1 @@
1
- {"version":3,"file":"compact.js","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"compact.js","names":[],"sources":["../../src/tabular/compact.ts"],"sourcesContent":["/**\n * @tempest-limits file-lines — the compact model format: the header, the column\n * dictionary, the quantised weights and the decoder that reads them back. Writer and\n * reader in one file because a format whose two halves live apart drifts.\n */\n/**\n * Running a model with no inference runtime at all.\n *\n * ONNX in the browser costs a **25.6 MB WebAssembly runtime** (6.0 MB\n * gzipped) before the first prediction, while the model itself is a few\n * hundred kilobytes. For an app whose only model is tabular, that runtime\n * *is* the download — so this reader replaces it with 1.49 KB of\n * arithmetic (measured, brotli).\n *\n * A linear model is a dot product. A tree is a chain of comparisons. That\n * is the whole implementation; there is nothing here that a WebAssembly\n * kernel would do better at this size.\n *\n * The file it reads (`.tmc`, magic `TMC1`) is written by\n * `tempest-fastapi-sdk`'s `export_sklearn_to_compact`, which verifies the\n * bytes against scikit-learn's own predictions and refuses to write a file\n * that disagrees. **It is data, not code**: no `eval`, no generated\n * JavaScript, nothing a strict CSP forbids.\n *\n * The trade against {@link TabularPredictor}: ONNX covers every estimator,\n * this covers linear models and tree ensembles. Pick by what your app\n * already ships — a page that loads ONNX for vision pays nothing extra for\n * tabular ONNX.\n */\n\nimport { CompactFormatError, FeatureShapeError, ModelFetchError } from \"./exceptions\";\nimport type { FeatureRow, PredictedLabel, TabularPrediction } from \"./types\";\n\n/** Magic bytes opening every compact file. */\nconst MAGIC = \"TMC1\";\n\n/** Layout version this reader understands. */\nexport const SUPPORTED_COMPACT_SCHEMA = 1;\n\n/** What the file holds. */\nexport type CompactKind = \"linear\" | \"tree_ensemble\";\n\n/** How raw scores become probabilities. */\ntype CompactLink = \"softmax\" | \"sigmoid\" | \"normalize\" | \"identity\";\n\n/** One array stored after the header. */\ninterface CompactSection {\n readonly name: string;\n readonly dtype: \"float32\" | \"int32\";\n readonly length: number;\n}\n\n/** The parsed header. */\ninterface CompactHeader {\n readonly schema_version: number;\n readonly kind: CompactKind;\n readonly task: \"classification\" | \"regression\";\n readonly link: CompactLink;\n readonly classes: readonly string[];\n /** How scikit-learn typed those labels: ``int``, ``float`` or ``str``. */\n readonly class_type?: \"int\" | \"float\" | \"str\";\n readonly n_features: number;\n readonly n_outputs: number;\n readonly n_trees?: number;\n readonly estimator: string;\n readonly feature_names: readonly string[];\n readonly preprocess: { readonly offset: number[]; readonly scale: number[] } | null;\n readonly sections: readonly CompactSection[];\n}\n\n/** What a loaded compact model is. */\nexport interface CompactPredictorInfo {\n /** Which reader path the file uses. */\n readonly kind: CompactKind;\n /** Class labels in score-column order; empty for a regressor. */\n readonly classes: readonly string[];\n /** Values expected per row. */\n readonly numFeatures: number;\n /** Column order recorded at training time, when the export had one. */\n readonly featureNames: readonly string[];\n /** Trees in the ensemble; `0` for a linear model. */\n readonly numTrees: number;\n /** Whether the model produces class scores. */\n readonly isClassifier: boolean;\n /** Class name of the exported estimator. */\n readonly estimator: string;\n}\n\n/**\n * A compact model, loaded and ready to answer.\n *\n * @example\n * ```ts\n * const predictor = await CompactPredictor.create(\"/models/risk.tmc\");\n * const { labels, probabilities } = await predictor.predict([[5.1, 3.5, 1.4, 0.2]]);\n * ```\n */\nexport class CompactPredictor {\n private constructor(\n private readonly header: CompactHeader,\n private readonly arrays: Record<string, Float32Array | Int32Array>,\n /** What is loaded. */\n public readonly info: CompactPredictorInfo,\n ) {}\n\n /**\n * Load a `.tmc` file.\n *\n * @param source A URL, or the bytes when the app already has them.\n * @param requestInit `fetch` options, when `source` is a URL.\n * @returns The loaded predictor.\n * @throws {@link ModelFetchError} when a URL cannot be read.\n * @throws {@link CompactFormatError} when the bytes are not a compact\n * model, or use a layout newer than this reader.\n */\n static async create(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n ): Promise<CompactPredictor> {\n const buffer = await toBuffer(source, requestInit);\n const bytes = new Uint8Array(buffer);\n\n if (String.fromCharCode(...bytes.subarray(0, 4)) !== MAGIC) {\n throw new CompactFormatError(\n \"This is not a compact model file: it does not start with \" +\n `\"${MAGIC}\". A .onnx file goes through TabularPredictor instead.`,\n );\n }\n\n const view = new DataView(buffer);\n const headerLength = view.getUint32(4, true);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)),\n ) as CompactHeader;\n\n if (header.schema_version > SUPPORTED_COMPACT_SCHEMA) {\n throw new CompactFormatError(\n `This file uses compact layout ${header.schema_version}, and this ` +\n `SDK understands ${SUPPORTED_COMPACT_SCHEMA}. Upgrade ` +\n \"tempest-react-sdk before serving it.\",\n );\n }\n\n const arrays: Record<string, Float32Array | Int32Array> = {};\n let cursor = 8 + headerLength;\n for (const section of header.sections) {\n arrays[section.name] =\n section.dtype === \"float32\"\n ? new Float32Array(buffer, cursor, section.length)\n : new Int32Array(buffer, cursor, section.length);\n cursor += section.length * 4;\n }\n\n return new CompactPredictor(header, arrays, {\n kind: header.kind,\n classes: header.classes,\n numFeatures: header.n_features,\n featureNames: header.feature_names,\n numTrees: header.n_trees ?? 0,\n isClassifier: header.task === \"classification\",\n estimator: header.estimator,\n });\n }\n\n /**\n * Predict for a batch of rows.\n *\n * @param rows One array of feature values per row, in training column\n * order. A single row is still wrapped: `[[...]]`.\n * @returns Labels, class scores when the model is a classifier, and the\n * call's duration.\n * @throws {@link FeatureShapeError} when the batch is empty, ragged, or\n * the wrong width.\n */\n async predict(rows: readonly FeatureRow[]): Promise<TabularPrediction> {\n const width = validateRows(rows, this.header.n_features);\n const started = performance.now();\n\n const scores: number[][] = [];\n for (const row of rows) {\n const prepared = this.preprocess(row, width);\n scores.push(\n this.header.kind === \"linear\"\n ? this.linearScores(prepared)\n : this.treeScores(prepared),\n );\n }\n\n const { labels, probabilities } = this.finish(scores);\n return {\n labels,\n probabilities,\n numRows: rows.length,\n ms: performance.now() - started,\n };\n }\n\n /** Releasing nothing, so callers can swap predictors without branching. */\n async dispose(): Promise<void> {\n /* no runtime, nothing to release */\n }\n\n /**\n * Apply the folded scaler, when the export had one.\n *\n * @param row The raw feature values.\n * @param width How many there are.\n * @returns The values the model was trained on.\n */\n private preprocess(row: FeatureRow, width: number): number[] {\n const preprocess = this.header.preprocess;\n if (preprocess === null) return row as number[];\n const scaled = new Array<number>(width);\n for (let index = 0; index < width; index += 1) {\n scaled[index] =\n ((row[index] as number) - (preprocess.offset[index] as number)) /\n (preprocess.scale[index] as number);\n }\n return scaled;\n }\n\n /**\n * Score one row against the coefficient matrix.\n *\n * @param row The prepared feature values.\n * @returns One raw score per output.\n */\n private linearScores(row: readonly number[]): number[] {\n const coef = this.arrays.coef as Float32Array;\n const intercept = this.arrays.intercept as Float32Array;\n const features = this.header.n_features;\n const outputs = this.header.n_outputs;\n\n const scores = new Array<number>(outputs);\n for (let output = 0; output < outputs; output += 1) {\n let total = intercept[output] as number;\n const base = output * features;\n for (let index = 0; index < features; index += 1) {\n total += (coef[base + index] as number) * (row[index] as number);\n }\n scores[output] = total;\n }\n return scores;\n }\n\n /**\n * Walk every tree and average what the leaves hold.\n *\n * A leaf is marked by a negative `feature` entry, which also carries\n * its slot in the value array — so the walk needs no second lookup and\n * the file stores values only for leaves.\n *\n * **The comparison runs in float32** (`Math.fround`), because that is\n * what scikit-learn does: it casts its input to float32 before\n * traversing, so a threshold like 5.099999904632568 — a float32 value\n * widened for storage — and an input of 5.1 compare *equal* and go\n * left. Comparing in float64 sends that row right instead, which on an\n * iris forest changed one tree's vote and moved a probability by 0.05.\n *\n * @param row The prepared feature values.\n * @returns One averaged score per output.\n */\n private treeScores(row: readonly number[]): number[] {\n const feature = this.arrays.node_feature as Int32Array;\n const threshold = this.arrays.node_threshold as Float32Array;\n const left = this.arrays.node_left as Int32Array;\n const right = this.arrays.node_right as Int32Array;\n const leaf = this.arrays.leaf_value as Float32Array;\n const offsets = this.arrays.tree_offset as Int32Array;\n const outputs = this.header.n_outputs;\n const trees = offsets.length - 1;\n\n const totals = new Array<number>(outputs).fill(0);\n for (let tree = 0; tree < trees; tree += 1) {\n let node = offsets[tree] as number;\n let column = feature[node] as number;\n while (column >= 0) {\n node =\n Math.fround(row[column] as number) <= (threshold[node] as number)\n ? (left[node] as number)\n : (right[node] as number);\n column = feature[node] as number;\n }\n const slot = (-1 - column) * outputs;\n for (let output = 0; output < outputs; output += 1) {\n totals[output] += leaf[slot + output] as number;\n }\n }\n for (let output = 0; output < outputs; output += 1) {\n totals[output] = (totals[output] as number) / trees;\n }\n return totals;\n }\n\n /**\n * Turn raw scores into labels and probabilities.\n *\n * @param scores One score array per row.\n * @returns Labels and probabilities in the shape the ONNX route uses,\n * so an app can swap runtimes without touching its own code. That\n * includes the label's **type**: an integer class comes back as a\n * number here exactly as ONNX returns it, because two routes over one\n * model that disagree on `0` versus `\"0\"` break the day someone\n * switches.\n */\n private finish(scores: readonly number[][]): {\n labels: PredictedLabel[];\n probabilities: number[][];\n } {\n const link = this.header.link;\n const classes = this.header.classes;\n\n if (link === \"identity\") {\n return { labels: scores.map((row) => row[0] as number), probabilities: [] };\n }\n\n const probabilities = scores.map((row) => {\n if (link === \"sigmoid\") {\n const positive = 1 / (1 + Math.exp(-(row[0] as number)));\n return [1 - positive, positive];\n }\n if (link === \"softmax\") {\n const highest = Math.max(...row);\n const exponentiated = row.map((value) => Math.exp(value - highest));\n const total = exponentiated.reduce((sum, value) => sum + value, 0);\n return exponentiated.map((value) => value / total);\n }\n const total = row.reduce((sum, value) => sum + value, 0);\n return total === 0 ? [...row] : row.map((value) => value / total);\n });\n\n const numeric = this.header.class_type !== \"str\";\n const labels = probabilities.map((row) => {\n let best = 0;\n for (let index = 1; index < row.length; index += 1) {\n if ((row[index] as number) > (row[best] as number)) best = index;\n }\n const label = classes[best];\n if (label === undefined) return best;\n return numeric ? Number(label) : label;\n });\n\n return { labels, probabilities };\n }\n}\n\n/**\n * Read a source into an `ArrayBuffer`.\n *\n * @param source A URL or the bytes.\n * @param requestInit `fetch` options.\n * @returns The bytes.\n * @throws {@link ModelFetchError} when the URL cannot be read.\n */\nasync function toBuffer(\n source: string | ArrayBuffer | Uint8Array,\n requestInit?: RequestInit,\n): Promise<ArrayBuffer> {\n if (typeof source === \"string\") {\n let response: Response;\n try {\n response = await fetch(source, requestInit);\n } catch (error) {\n throw new ModelFetchError(`Could not download the model: ${source}`, {\n cause: error,\n });\n }\n if (!response.ok) {\n throw new ModelFetchError(\n `Could not download the model: ${response.status} ${response.statusText}`,\n );\n }\n return await response.arrayBuffer();\n }\n if (source instanceof Uint8Array) {\n return source.buffer.slice(\n source.byteOffset,\n source.byteOffset + source.byteLength,\n ) as ArrayBuffer;\n }\n return source;\n}\n\n/**\n * Check a batch before predicting on it.\n *\n * @param rows The batch.\n * @param expected Features the model wants per row.\n * @returns The batch width.\n * @throws {@link FeatureShapeError} when the batch cannot be predicted on.\n */\nfunction validateRows(rows: readonly FeatureRow[], expected: number): number {\n if (!Array.isArray(rows) || rows.length === 0) {\n throw new FeatureShapeError(\"predict() needs at least one row, shaped [[f1, f2, ...]].\");\n }\n const width = rows[0]?.length ?? 0;\n const ragged = rows.findIndex((row) => row.length !== width);\n if (ragged !== -1) {\n throw new FeatureShapeError(\n `All rows must have the same width; row ${ragged} has ` +\n `${rows[ragged]?.length} values, expected ${width}.`,\n );\n }\n if (width !== expected) {\n throw new FeatureShapeError(\n `The model expects ${expected} features per row, got ${width}.`,\n );\n }\n return width;\n}\n"],"mappings":"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"}