@azurioh/discord-kernel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +77 -0
  3. package/dist/authz/authorizer.d.ts +45 -0
  4. package/dist/authz/authorizer.d.ts.map +1 -0
  5. package/dist/authz/authorizer.js +31 -0
  6. package/dist/authz/authorizer.js.map +1 -0
  7. package/dist/authz/require-level-guard.d.ts +23 -0
  8. package/dist/authz/require-level-guard.d.ts.map +1 -0
  9. package/dist/authz/require-level-guard.js +37 -0
  10. package/dist/authz/require-level-guard.js.map +1 -0
  11. package/dist/clock.d.ts +13 -0
  12. package/dist/clock.d.ts.map +1 -0
  13. package/dist/clock.js +13 -0
  14. package/dist/clock.js.map +1 -0
  15. package/dist/concurrency/keyed-queue.d.ts +18 -0
  16. package/dist/concurrency/keyed-queue.d.ts.map +1 -0
  17. package/dist/concurrency/keyed-queue.js +37 -0
  18. package/dist/concurrency/keyed-queue.js.map +1 -0
  19. package/dist/concurrency/run-with-concurrency.d.ts +15 -0
  20. package/dist/concurrency/run-with-concurrency.d.ts.map +1 -0
  21. package/dist/concurrency/run-with-concurrency.js +40 -0
  22. package/dist/concurrency/run-with-concurrency.js.map +1 -0
  23. package/dist/config/env.d.ts +17 -0
  24. package/dist/config/env.d.ts.map +1 -0
  25. package/dist/config/env.js +49 -0
  26. package/dist/config/env.js.map +1 -0
  27. package/dist/config/errors.d.ts +14 -0
  28. package/dist/config/errors.d.ts.map +1 -0
  29. package/dist/config/errors.js +25 -0
  30. package/dist/config/errors.js.map +1 -0
  31. package/dist/datetime.d.ts +34 -0
  32. package/dist/datetime.d.ts.map +1 -0
  33. package/dist/datetime.js +188 -0
  34. package/dist/datetime.js.map +1 -0
  35. package/dist/discord/api-errors.d.ts +20 -0
  36. package/dist/discord/api-errors.d.ts.map +1 -0
  37. package/dist/discord/api-errors.js +90 -0
  38. package/dist/discord/api-errors.js.map +1 -0
  39. package/dist/discord/channel-exporter.d.ts +42 -0
  40. package/dist/discord/channel-exporter.d.ts.map +1 -0
  41. package/dist/discord/channel-exporter.js +3 -0
  42. package/dist/discord/channel-exporter.js.map +1 -0
  43. package/dist/discord/command/autocomplete-dispatcher.d.ts +26 -0
  44. package/dist/discord/command/autocomplete-dispatcher.d.ts.map +1 -0
  45. package/dist/discord/command/autocomplete-dispatcher.js +41 -0
  46. package/dist/discord/command/autocomplete-dispatcher.js.map +1 -0
  47. package/dist/discord/command/autocomplete.d.ts +18 -0
  48. package/dist/discord/command/autocomplete.d.ts.map +1 -0
  49. package/dist/discord/command/autocomplete.js +65 -0
  50. package/dist/discord/command/autocomplete.js.map +1 -0
  51. package/dist/discord/command/context.d.ts +33 -0
  52. package/dist/discord/command/context.d.ts.map +1 -0
  53. package/dist/discord/command/context.js +48 -0
  54. package/dist/discord/command/context.js.map +1 -0
  55. package/dist/discord/command/cooldown-guard.d.ts +25 -0
  56. package/dist/discord/command/cooldown-guard.d.ts.map +1 -0
  57. package/dist/discord/command/cooldown-guard.js +60 -0
  58. package/dist/discord/command/cooldown-guard.js.map +1 -0
  59. package/dist/discord/command/create-command.d.ts +100 -0
  60. package/dist/discord/command/create-command.d.ts.map +1 -0
  61. package/dist/discord/command/create-command.js +245 -0
  62. package/dist/discord/command/create-command.js.map +1 -0
  63. package/dist/discord/command/create-context-menu-command.d.ts +51 -0
  64. package/dist/discord/command/create-context-menu-command.d.ts.map +1 -0
  65. package/dist/discord/command/create-context-menu-command.js +128 -0
  66. package/dist/discord/command/create-context-menu-command.js.map +1 -0
  67. package/dist/discord/command/errors.d.ts +19 -0
  68. package/dist/discord/command/errors.d.ts.map +1 -0
  69. package/dist/discord/command/errors.js +27 -0
  70. package/dist/discord/command/errors.js.map +1 -0
  71. package/dist/discord/command/guard.d.ts +30 -0
  72. package/dist/discord/command/guard.d.ts.map +1 -0
  73. package/dist/discord/command/guard.js +34 -0
  74. package/dist/discord/command/guard.js.map +1 -0
  75. package/dist/discord/command/localization.d.ts +9 -0
  76. package/dist/discord/command/localization.d.ts.map +1 -0
  77. package/dist/discord/command/localization.js +13 -0
  78. package/dist/discord/command/localization.js.map +1 -0
  79. package/dist/discord/command/options.d.ts +113 -0
  80. package/dist/discord/command/options.d.ts.map +1 -0
  81. package/dist/discord/command/options.js +509 -0
  82. package/dist/discord/command/options.js.map +1 -0
  83. package/dist/discord/command/permission-guard.d.ts +21 -0
  84. package/dist/discord/command/permission-guard.d.ts.map +1 -0
  85. package/dist/discord/command/permission-guard.js +69 -0
  86. package/dist/discord/command/permission-guard.js.map +1 -0
  87. package/dist/discord/command/router.d.ts +48 -0
  88. package/dist/discord/command/router.d.ts.map +1 -0
  89. package/dist/discord/command/router.js +118 -0
  90. package/dist/discord/command/router.js.map +1 -0
  91. package/dist/discord/command/types.d.ts +51 -0
  92. package/dist/discord/command/types.d.ts.map +1 -0
  93. package/dist/discord/command/types.js +3 -0
  94. package/dist/discord/command/types.js.map +1 -0
  95. package/dist/discord/components/component-router.d.ts +116 -0
  96. package/dist/discord/components/component-router.d.ts.map +1 -0
  97. package/dist/discord/components/component-router.js +123 -0
  98. package/dist/discord/components/component-router.js.map +1 -0
  99. package/dist/discord/components/interactive-message/index.d.ts +3 -0
  100. package/dist/discord/components/interactive-message/index.d.ts.map +1 -0
  101. package/dist/discord/components/interactive-message/index.js +11 -0
  102. package/dist/discord/components/interactive-message/index.js.map +1 -0
  103. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts +195 -0
  104. package/dist/discord/components/interactive-message/interactive-message-collector.d.ts.map +1 -0
  105. package/dist/discord/components/interactive-message/interactive-message-collector.js +151 -0
  106. package/dist/discord/components/interactive-message/interactive-message-collector.js.map +1 -0
  107. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts +30 -0
  108. package/dist/discord/components/interactive-message/mount-interactive-message.d.ts.map +1 -0
  109. package/dist/discord/components/interactive-message/mount-interactive-message.js +44 -0
  110. package/dist/discord/components/interactive-message/mount-interactive-message.js.map +1 -0
  111. package/dist/discord/components/paginator/index.d.ts +5 -0
  112. package/dist/discord/components/paginator/index.d.ts.map +1 -0
  113. package/dist/discord/components/paginator/index.js +15 -0
  114. package/dist/discord/components/paginator/index.js.map +1 -0
  115. package/dist/discord/components/paginator/mount-paginator.d.ts +25 -0
  116. package/dist/discord/components/paginator/mount-paginator.d.ts.map +1 -0
  117. package/dist/discord/components/paginator/mount-paginator.js +41 -0
  118. package/dist/discord/components/paginator/mount-paginator.js.map +1 -0
  119. package/dist/discord/components/paginator/page-state.d.ts +21 -0
  120. package/dist/discord/components/paginator/page-state.d.ts.map +1 -0
  121. package/dist/discord/components/paginator/page-state.js +44 -0
  122. package/dist/discord/components/paginator/page-state.js.map +1 -0
  123. package/dist/discord/components/paginator/paginator-buttons.d.ts +17 -0
  124. package/dist/discord/components/paginator/paginator-buttons.d.ts.map +1 -0
  125. package/dist/discord/components/paginator/paginator-buttons.js +45 -0
  126. package/dist/discord/components/paginator/paginator-buttons.js.map +1 -0
  127. package/dist/discord/components/paginator/paginator-view.d.ts +26 -0
  128. package/dist/discord/components/paginator/paginator-view.d.ts.map +1 -0
  129. package/dist/discord/components/paginator/paginator-view.js +28 -0
  130. package/dist/discord/components/paginator/paginator-view.js.map +1 -0
  131. package/dist/discord/components/persistent-paginator.d.ts +47 -0
  132. package/dist/discord/components/persistent-paginator.d.ts.map +1 -0
  133. package/dist/discord/components/persistent-paginator.js +137 -0
  134. package/dist/discord/components/persistent-paginator.js.map +1 -0
  135. package/dist/discord/components/settings-editor/index.d.ts +7 -0
  136. package/dist/discord/components/settings-editor/index.d.ts.map +1 -0
  137. package/dist/discord/components/settings-editor/index.js +22 -0
  138. package/dist/discord/components/settings-editor/index.js.map +1 -0
  139. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts +140 -0
  140. package/dist/discord/components/settings-editor/mount-settings-editor.d.ts.map +1 -0
  141. package/dist/discord/components/settings-editor/mount-settings-editor.js +344 -0
  142. package/dist/discord/components/settings-editor/mount-settings-editor.js.map +1 -0
  143. package/dist/discord/components/settings-editor/navigation.d.ts +49 -0
  144. package/dist/discord/components/settings-editor/navigation.d.ts.map +1 -0
  145. package/dist/discord/components/settings-editor/navigation.js +39 -0
  146. package/dist/discord/components/settings-editor/navigation.js.map +1 -0
  147. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts +56 -0
  148. package/dist/discord/components/settings-editor/settings-editor-card.view.d.ts.map +1 -0
  149. package/dist/discord/components/settings-editor/settings-editor-card.view.js +284 -0
  150. package/dist/discord/components/settings-editor/settings-editor-card.view.js.map +1 -0
  151. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts +69 -0
  152. package/dist/discord/components/settings-editor/settings-editor-field-modal.d.ts.map +1 -0
  153. package/dist/discord/components/settings-editor/settings-editor-field-modal.js +353 -0
  154. package/dist/discord/components/settings-editor/settings-editor-field-modal.js.map +1 -0
  155. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts +216 -0
  156. package/dist/discord/components/settings-editor/settings-editor-fields.d.ts.map +1 -0
  157. package/dist/discord/components/settings-editor/settings-editor-fields.js +66 -0
  158. package/dist/discord/components/settings-editor/settings-editor-fields.js.map +1 -0
  159. package/dist/discord/components/settings-editor/settings-editor.view.d.ts +303 -0
  160. package/dist/discord/components/settings-editor/settings-editor.view.d.ts.map +1 -0
  161. package/dist/discord/components/settings-editor/settings-editor.view.js +316 -0
  162. package/dist/discord/components/settings-editor/settings-editor.view.js.map +1 -0
  163. package/dist/discord/events/create-event.d.ts +5 -0
  164. package/dist/discord/events/create-event.d.ts.map +1 -0
  165. package/dist/discord/events/create-event.js +8 -0
  166. package/dist/discord/events/create-event.js.map +1 -0
  167. package/dist/discord/events/event-router.d.ts +16 -0
  168. package/dist/discord/events/event-router.d.ts.map +1 -0
  169. package/dist/discord/events/event-router.js +51 -0
  170. package/dist/discord/events/event-router.js.map +1 -0
  171. package/dist/discord/events/types.d.ts +16 -0
  172. package/dist/discord/events/types.d.ts.map +1 -0
  173. package/dist/discord/events/types.js +3 -0
  174. package/dist/discord/events/types.js.map +1 -0
  175. package/dist/discord/i18n.d.ts +58 -0
  176. package/dist/discord/i18n.d.ts.map +1 -0
  177. package/dist/discord/i18n.js +173 -0
  178. package/dist/discord/i18n.js.map +1 -0
  179. package/dist/discord/interaction/button.d.ts +39 -0
  180. package/dist/discord/interaction/button.d.ts.map +1 -0
  181. package/dist/discord/interaction/button.js +36 -0
  182. package/dist/discord/interaction/button.js.map +1 -0
  183. package/dist/discord/interaction/interaction-router.d.ts +21 -0
  184. package/dist/discord/interaction/interaction-router.d.ts.map +1 -0
  185. package/dist/discord/interaction/interaction-router.js +24 -0
  186. package/dist/discord/interaction/interaction-router.js.map +1 -0
  187. package/dist/discord/interaction/modal.d.ts +171 -0
  188. package/dist/discord/interaction/modal.d.ts.map +1 -0
  189. package/dist/discord/interaction/modal.js +264 -0
  190. package/dist/discord/interaction/modal.js.map +1 -0
  191. package/dist/discord/interaction/select-menu.d.ts +104 -0
  192. package/dist/discord/interaction/select-menu.d.ts.map +1 -0
  193. package/dist/discord/interaction/select-menu.js +126 -0
  194. package/dist/discord/interaction/select-menu.js.map +1 -0
  195. package/dist/discord/permissions.d.ts +32 -0
  196. package/dist/discord/permissions.d.ts.map +1 -0
  197. package/dist/discord/permissions.js +40 -0
  198. package/dist/discord/permissions.js.map +1 -0
  199. package/dist/discord/placeholder-registry-errors.d.ts +10 -0
  200. package/dist/discord/placeholder-registry-errors.d.ts.map +1 -0
  201. package/dist/discord/placeholder-registry-errors.js +18 -0
  202. package/dist/discord/placeholder-registry-errors.js.map +1 -0
  203. package/dist/discord/placeholder-registry.d.ts +53 -0
  204. package/dist/discord/placeholder-registry.d.ts.map +1 -0
  205. package/dist/discord/placeholder-registry.js +52 -0
  206. package/dist/discord/placeholder-registry.js.map +1 -0
  207. package/dist/discord/presenter.d.ts +26 -0
  208. package/dist/discord/presenter.d.ts.map +1 -0
  209. package/dist/discord/presenter.js +3 -0
  210. package/dist/discord/presenter.js.map +1 -0
  211. package/dist/discord/ui/card.d.ts +114 -0
  212. package/dist/discord/ui/card.d.ts.map +1 -0
  213. package/dist/discord/ui/card.js +162 -0
  214. package/dist/discord/ui/card.js.map +1 -0
  215. package/dist/discord/ui/color-input-errors.d.ts +12 -0
  216. package/dist/discord/ui/color-input-errors.d.ts.map +1 -0
  217. package/dist/discord/ui/color-input-errors.js +21 -0
  218. package/dist/discord/ui/color-input-errors.js.map +1 -0
  219. package/dist/discord/ui/color-input.d.ts +37 -0
  220. package/dist/discord/ui/color-input.d.ts.map +1 -0
  221. package/dist/discord/ui/color-input.js +166 -0
  222. package/dist/discord/ui/color-input.js.map +1 -0
  223. package/dist/discord/ui/colors.d.ts +44 -0
  224. package/dist/discord/ui/colors.d.ts.map +1 -0
  225. package/dist/discord/ui/colors.js +45 -0
  226. package/dist/discord/ui/colors.js.map +1 -0
  227. package/dist/discord/ui/embed.d.ts +16 -0
  228. package/dist/discord/ui/embed.d.ts.map +1 -0
  229. package/dist/discord/ui/embed.js +53 -0
  230. package/dist/discord/ui/embed.js.map +1 -0
  231. package/dist/discord/ui/message-attachments.d.ts +40 -0
  232. package/dist/discord/ui/message-attachments.d.ts.map +1 -0
  233. package/dist/discord/ui/message-attachments.js +27 -0
  234. package/dist/discord/ui/message-attachments.js.map +1 -0
  235. package/dist/errors/business-error.d.ts +41 -0
  236. package/dist/errors/business-error.d.ts.map +1 -0
  237. package/dist/errors/business-error.js +39 -0
  238. package/dist/errors/business-error.js.map +1 -0
  239. package/dist/errors/describe-error.d.ts +29 -0
  240. package/dist/errors/describe-error.d.ts.map +1 -0
  241. package/dist/errors/describe-error.js +49 -0
  242. package/dist/errors/describe-error.js.map +1 -0
  243. package/dist/errors/incident-ref.d.ts +7 -0
  244. package/dist/errors/incident-ref.d.ts.map +1 -0
  245. package/dist/errors/incident-ref.js +13 -0
  246. package/dist/errors/incident-ref.js.map +1 -0
  247. package/dist/errors/index.d.ts +4 -0
  248. package/dist/errors/index.d.ts.map +1 -0
  249. package/dist/errors/index.js +15 -0
  250. package/dist/errors/index.js.map +1 -0
  251. package/dist/i18n/business-message.d.ts +14 -0
  252. package/dist/i18n/business-message.d.ts.map +1 -0
  253. package/dist/i18n/business-message.js +19 -0
  254. package/dist/i18n/business-message.js.map +1 -0
  255. package/dist/i18n/catalog.d.ts +27 -0
  256. package/dist/i18n/catalog.d.ts.map +1 -0
  257. package/dist/i18n/catalog.js +25 -0
  258. package/dist/i18n/catalog.js.map +1 -0
  259. package/dist/i18n/errors.d.ts +10 -0
  260. package/dist/i18n/errors.d.ts.map +1 -0
  261. package/dist/i18n/errors.js +18 -0
  262. package/dist/i18n/errors.js.map +1 -0
  263. package/dist/i18n/index.d.ts +6 -0
  264. package/dist/i18n/index.d.ts.map +1 -0
  265. package/dist/i18n/index.js +16 -0
  266. package/dist/i18n/index.js.map +1 -0
  267. package/dist/i18n/locale.d.ts +22 -0
  268. package/dist/i18n/locale.d.ts.map +1 -0
  269. package/dist/i18n/locale.js +40 -0
  270. package/dist/i18n/locale.js.map +1 -0
  271. package/dist/i18n/translator.d.ts +35 -0
  272. package/dist/i18n/translator.d.ts.map +1 -0
  273. package/dist/i18n/translator.js +40 -0
  274. package/dist/i18n/translator.js.map +1 -0
  275. package/dist/logger.d.ts +28 -0
  276. package/dist/logger.d.ts.map +1 -0
  277. package/dist/logger.js +3 -0
  278. package/dist/logger.js.map +1 -0
  279. package/dist/module/module.d.ts +42 -0
  280. package/dist/module/module.d.ts.map +1 -0
  281. package/dist/module/module.js +3 -0
  282. package/dist/module/module.js.map +1 -0
  283. package/dist/persistence/database.d.ts +24 -0
  284. package/dist/persistence/database.d.ts.map +1 -0
  285. package/dist/persistence/database.js +3 -0
  286. package/dist/persistence/database.js.map +1 -0
  287. package/dist/resilience/retry.d.ts +36 -0
  288. package/dist/resilience/retry.d.ts.map +1 -0
  289. package/dist/resilience/retry.js +78 -0
  290. package/dist/resilience/retry.js.map +1 -0
  291. package/dist/scheduler/errors.d.ts +25 -0
  292. package/dist/scheduler/errors.d.ts.map +1 -0
  293. package/dist/scheduler/errors.js +45 -0
  294. package/dist/scheduler/errors.js.map +1 -0
  295. package/dist/scheduler/scheduler.d.ts +68 -0
  296. package/dist/scheduler/scheduler.d.ts.map +1 -0
  297. package/dist/scheduler/scheduler.js +162 -0
  298. package/dist/scheduler/scheduler.js.map +1 -0
  299. package/package.json +89 -0
  300. package/src/authz/authorizer.ts +76 -0
  301. package/src/authz/require-level-guard.ts +42 -0
  302. package/src/clock.ts +18 -0
  303. package/src/concurrency/keyed-queue.ts +50 -0
  304. package/src/concurrency/run-with-concurrency.ts +44 -0
  305. package/src/config/env.ts +58 -0
  306. package/src/config/errors.ts +21 -0
  307. package/src/datetime.ts +221 -0
  308. package/src/discord/api-errors.ts +91 -0
  309. package/src/discord/channel-exporter.ts +48 -0
  310. package/src/discord/command/autocomplete-dispatcher.ts +54 -0
  311. package/src/discord/command/autocomplete.ts +76 -0
  312. package/src/discord/command/context.ts +85 -0
  313. package/src/discord/command/cooldown-guard.ts +81 -0
  314. package/src/discord/command/create-command.ts +431 -0
  315. package/src/discord/command/create-context-menu-command.ts +227 -0
  316. package/src/discord/command/errors.ts +22 -0
  317. package/src/discord/command/guard.ts +54 -0
  318. package/src/discord/command/localization.ts +11 -0
  319. package/src/discord/command/options.ts +847 -0
  320. package/src/discord/command/permission-guard.ts +74 -0
  321. package/src/discord/command/router.ts +142 -0
  322. package/src/discord/command/types.ts +52 -0
  323. package/src/discord/components/component-router.ts +213 -0
  324. package/src/discord/components/interactive-message/index.ts +18 -0
  325. package/src/discord/components/interactive-message/interactive-message-collector.ts +374 -0
  326. package/src/discord/components/interactive-message/mount-interactive-message.ts +71 -0
  327. package/src/discord/components/paginator/index.ts +17 -0
  328. package/src/discord/components/paginator/mount-paginator.ts +69 -0
  329. package/src/discord/components/paginator/page-state.ts +45 -0
  330. package/src/discord/components/paginator/paginator-buttons.ts +60 -0
  331. package/src/discord/components/paginator/paginator-view.ts +61 -0
  332. package/src/discord/components/persistent-paginator.ts +197 -0
  333. package/src/discord/components/settings-editor/index.ts +41 -0
  334. package/src/discord/components/settings-editor/mount-settings-editor.ts +646 -0
  335. package/src/discord/components/settings-editor/navigation.ts +67 -0
  336. package/src/discord/components/settings-editor/settings-editor-card.view.ts +344 -0
  337. package/src/discord/components/settings-editor/settings-editor-field-modal.ts +538 -0
  338. package/src/discord/components/settings-editor/settings-editor-fields.ts +290 -0
  339. package/src/discord/components/settings-editor/settings-editor.view.ts +654 -0
  340. package/src/discord/events/create-event.ts +7 -0
  341. package/src/discord/events/event-router.ts +53 -0
  342. package/src/discord/events/types.ts +17 -0
  343. package/src/discord/i18n.ts +173 -0
  344. package/src/discord/interaction/button.ts +75 -0
  345. package/src/discord/interaction/interaction-router.ts +28 -0
  346. package/src/discord/interaction/modal.ts +505 -0
  347. package/src/discord/interaction/select-menu.ts +246 -0
  348. package/src/discord/permissions.ts +57 -0
  349. package/src/discord/placeholder-registry-errors.ts +11 -0
  350. package/src/discord/placeholder-registry.ts +77 -0
  351. package/src/discord/presenter.ts +26 -0
  352. package/src/discord/ui/card.ts +303 -0
  353. package/src/discord/ui/color-input-errors.ts +17 -0
  354. package/src/discord/ui/color-input.ts +179 -0
  355. package/src/discord/ui/colors.ts +58 -0
  356. package/src/discord/ui/embed.ts +64 -0
  357. package/src/discord/ui/message-attachments.ts +57 -0
  358. package/src/errors/business-error.ts +49 -0
  359. package/src/errors/describe-error.ts +73 -0
  360. package/src/errors/incident-ref.ts +10 -0
  361. package/src/errors/index.ts +12 -0
  362. package/src/i18n/business-message.ts +23 -0
  363. package/src/i18n/catalog.ts +41 -0
  364. package/src/i18n/errors.ts +11 -0
  365. package/src/i18n/index.ts +11 -0
  366. package/src/i18n/locale.ts +41 -0
  367. package/src/i18n/translator.ts +78 -0
  368. package/src/logger.ts +28 -0
  369. package/src/module/module.ts +42 -0
  370. package/src/persistence/database.ts +23 -0
  371. package/src/resilience/retry.ts +102 -0
  372. package/src/scheduler/errors.ts +40 -0
  373. package/src/scheduler/scheduler.ts +248 -0
@@ -0,0 +1,374 @@
1
+ import type {
2
+ ActionRowBuilder,
3
+ AnySelectMenuInteraction,
4
+ Attachment,
5
+ ButtonInteraction,
6
+ ChatInputCommandInteraction,
7
+ CollectedMessageInteraction,
8
+ ContainerBuilder,
9
+ EmbedBuilder,
10
+ InteractionUpdateOptions,
11
+ MessageActionRowComponentBuilder,
12
+ MessageMentionOptions,
13
+ } from "discord.js";
14
+ import type { Button, ComponentContext } from "@/discord/interaction/button";
15
+ import type { Card } from "@/discord/ui/card";
16
+ import { type MessageFile, planMessageAttachments } from "@/discord/ui/message-attachments";
17
+ import { describeError } from "@/errors";
18
+ import type { Logger } from "@/logger";
19
+
20
+ /** How long an interactive message stays live without a click. */
21
+ export const COLLECTOR_IDLE_MS = 5 * 60 * 1000;
22
+
23
+ /**
24
+ * What every layout a message can render shares: the files it carries and who
25
+ * it may notify.
26
+ */
27
+ interface InteractiveMessagePayloadBase {
28
+ /**
29
+ * The files this message carries, as its **whole** set — an embed or a
30
+ * card's `thumbnail`/`gallery` refers to one by name, as
31
+ * `attachment://<name>`.
32
+ *
33
+ * Leaving the key out means "leave the message's attachments alone", which
34
+ * is what a view carrying no file of its own must do: stating an empty set
35
+ * would strip whatever the message was sent with. A view that does own its
36
+ * files states them on every render, empty included, since that is what
37
+ * makes removing one show up.
38
+ */
39
+ files?: MessageFile[];
40
+ /**
41
+ * Who this message may notify.
42
+ *
43
+ * Stated by a view that renders text it does not own — user-authored content
44
+ * carrying mentions — so looking at a preview never pings the people it
45
+ * names. Left out means Discord's own default, which is what a view building
46
+ * every string itself wants.
47
+ */
48
+ allowedMentions?: MessageMentionOptions;
49
+ }
50
+
51
+ /**
52
+ * The message payload shape shared by the initial reply and every update.
53
+ *
54
+ * A discriminated union rather than one shape with an optional `card`:
55
+ * Discord's `IS_COMPONENTS_V2` flag is **irreversible** (once a message carries
56
+ * it, an edit can never clear it) and a message carrying it may hold **no**
57
+ * embed at all — so "embeds" and "card" are not two options on the same
58
+ * message, they are two kinds of message, and mixing their fields must fail to
59
+ * compile rather than build a payload Discord would refuse.
60
+ *
61
+ * `layout` defaults to `"embeds"` — it is optional on that branch alone — so
62
+ * every payload a view already returns (`{ embeds, components }`, no `layout`
63
+ * key at all) keeps satisfying this type unchanged: nothing that predates the
64
+ * card layout has to name the one it was always using.
65
+ */
66
+ export type InteractiveMessagePayload<S = never> =
67
+ | (InteractiveMessagePayloadBase & {
68
+ layout?: "embeds";
69
+ embeds: EmbedBuilder[];
70
+ components: ActionRowBuilder<MessageActionRowComponentBuilder>[];
71
+ })
72
+ | (InteractiveMessagePayloadBase & {
73
+ layout: "card";
74
+ card: Card<S>;
75
+ /**
76
+ * Rows shown beside the card rather than inside it — a screen's own
77
+ * controls (reset, back, done). A Components V2 message accepts several
78
+ * top-level components: the `Container` `card` compiles to is one, and
79
+ * each of these is an `ActionRow` next to it. Omitted, or empty, for a
80
+ * payload with no screen-level controls to show right now (a closed
81
+ * screen).
82
+ */
83
+ controls?: ActionRowBuilder<MessageActionRowComponentBuilder>[];
84
+ /**
85
+ * Never present on this branch — declared anyway so that
86
+ * `InteractiveMessagePayload<S>["components"]`, used elsewhere purely to
87
+ * name the action-row array type, keeps resolving instead of erroring on
88
+ * a key one arm of the union does not have.
89
+ */
90
+ components?: never;
91
+ });
92
+
93
+ /**
94
+ * The payload as discord.js takes it on an edit, given what the message already
95
+ * carries: a file already up there under a wanted name is kept rather than
96
+ * uploaded a second time, so an unrelated re-render neither drops nor re-sends
97
+ * it.
98
+ *
99
+ * A payload stating no files touches neither field, leaving the message's own
100
+ * attachments alone.
101
+ *
102
+ * `state` is only read for a `"card"` payload — it is what `Card.apply` needs
103
+ * to render the container — and is otherwise unused. No caller building an
104
+ * `"embeds"` payload has ever had to pass it, and still does not: it is the
105
+ * third, optional parameter existing two-argument calls already omit.
106
+ */
107
+ export function toMessageEditOptions<S>(
108
+ payload: InteractiveMessagePayload<S>,
109
+ existing: readonly Attachment[],
110
+ state?: S,
111
+ ): InteractionUpdateOptions {
112
+ const { files, allowedMentions } = payload;
113
+ const mentions = allowedMentions === undefined ? {} : { allowedMentions };
114
+ const attachments = files === undefined ? {} : planMessageAttachments(existing, files);
115
+
116
+ if (payload.layout === "card") {
117
+ if (state === undefined) {
118
+ // Only reachable once a caller actually builds a `"card"` payload
119
+ // without threading the state its `Card` needs — a programming error
120
+ // in that caller, not something a member's click can trigger.
121
+ throw new Error(
122
+ 'toMessageEditOptions: a "card" layout payload needs the state its Card was ' +
123
+ "declared over to call Card.apply, and none was given.",
124
+ );
125
+ }
126
+ return {
127
+ components: [payload.card.apply(state, false), ...(payload.controls ?? [])],
128
+ ...mentions,
129
+ ...attachments,
130
+ };
131
+ }
132
+
133
+ const { embeds, components } = payload;
134
+ return { embeds, components, ...mentions, ...attachments };
135
+ }
136
+
137
+ /**
138
+ * The single mutable cell shared by the view and the collector. Kept explicit
139
+ * rather than captured as a closure variable so the two always read the same
140
+ * reference.
141
+ */
142
+ export interface StateStore<S> {
143
+ read(): S;
144
+ write(next: S): void;
145
+ }
146
+
147
+ /** Hold a state the collector can advance in place. */
148
+ export function createStateStore<S>(initial: S): StateStore<S> {
149
+ let state = initial;
150
+ return {
151
+ read: () => state,
152
+ write: (next) => {
153
+ state = next;
154
+ },
155
+ };
156
+ }
157
+
158
+ /**
159
+ * What {@link InteractiveView.disabledControls} hands back: enough of a payload
160
+ * to neutralise the message's controls in place, in whichever layout the view
161
+ * renders.
162
+ *
163
+ * Not `Pick<InteractiveMessagePayload<S>, "components">` alone — on the card
164
+ * branch that type is `{ components?: never }`, since a card carries `render`'s
165
+ * controls beside its one container rather than inside it, as top-level
166
+ * `components` of its own kind. The second arm is what lets a card screen hand
167
+ * back its neutralised container *and* its neutralised outer row together —
168
+ * `Card.apply(state, true)` for the former, the same `createActionRow(...,
169
+ * { disableAll: true })` `render` already builds the row with for the latter —
170
+ * see {@link import("@/discord/ui/card").Card}.
171
+ */
172
+ export type DisabledInteractiveMessageComponents<S> =
173
+ | Pick<InteractiveMessagePayload<S>, "components">
174
+ | {
175
+ readonly components: readonly (
176
+ | ContainerBuilder
177
+ | ActionRowBuilder<MessageActionRowComponentBuilder>
178
+ )[];
179
+ };
180
+
181
+ /**
182
+ * Turns a state into a message payload. Everything sent to Discord goes through
183
+ * here, so the collector never builds a payload of its own and the initial
184
+ * render can never drift from the updates.
185
+ */
186
+ export interface InteractiveView<S> {
187
+ render(state: S): InteractiveMessagePayload<S>;
188
+ disabledControls(state: S): DisabledInteractiveMessageComponents<S>;
189
+ }
190
+
191
+ /**
192
+ * What a pick hands to its handler: the triggering interaction, the values
193
+ * Discord sent back already unwrapped, and the same `state`/`update` pairing a
194
+ * button's {@link ComponentContext} carries.
195
+ */
196
+ export interface SelectComponentContext<S> {
197
+ readonly interaction: AnySelectMenuInteraction;
198
+ readonly values: readonly string[];
199
+ readonly state: S;
200
+ update(next: S): Promise<void>;
201
+ }
202
+
203
+ /**
204
+ * A select menu's behaviour inside an interactive message: the custom id the
205
+ * collector routes on, and what a pick does with the current state.
206
+ *
207
+ * Only the behaviour, unlike {@link Button} which also renders itself: a
208
+ * compiled select menu's rendering does not vary with the state, so it stays
209
+ * where the rest of the message is built — in the view.
210
+ */
211
+ export interface InteractiveSelect<S> {
212
+ readonly id: string;
213
+ onSelect(ctx: SelectComponentContext<S>): Promise<void> | void;
214
+ }
215
+
216
+ export interface InteractiveMessageCollectorDeps<S> {
217
+ interaction: ChatInputCommandInteraction;
218
+ response: Awaited<ReturnType<ChatInputCommandInteraction["editReply"]>>;
219
+ buttons: Button<S>[];
220
+ /** Omitted by a message whose controls are all buttons. */
221
+ selects?: InteractiveSelect<S>[];
222
+ view: InteractiveView<S>;
223
+ store: StateStore<S>;
224
+ ownerId: string;
225
+ idleMs: number;
226
+ /**
227
+ * Run once the message stops reacting, whichever way it ended — an explicit
228
+ * {@link InteractiveMessageHandle.stop} or the idle deadline.
229
+ *
230
+ * This is where work that must not repeat at every click belongs: a settings
231
+ * screen pushing its result to whatever it configures, so what it configures
232
+ * never shows the half-finished states an administrator passed through. A
233
+ * failure is logged, never thrown — the message has already ended.
234
+ */
235
+ onEnd?: () => Promise<void> | void;
236
+ logger: Logger;
237
+ }
238
+
239
+ /**
240
+ * Attach the interaction/idle wiring to an already-sent message: it owns the
241
+ * collector, routes each interaction by custom ID to the button's `onClick` or
242
+ * the select menu's `onSelect`, scopes every interaction to the owner, and
243
+ * centralises error handling. Mirrors command `dispatch`.
244
+ *
245
+ * Generic over the state so the same mechanism serves anything whose controls
246
+ * react for a bounded lifetime — pagination, a settings screen, a confirmation.
247
+ * Persistent components that must survive a restart belong to the
248
+ * {@link import("@/discord/components/component-router").ComponentRouter}
249
+ * instead: this state lives in memory and dies on idle.
250
+ */
251
+ export interface InteractiveMessageHandle {
252
+ /**
253
+ * End the message's interactive life **without** neutralising its controls.
254
+ *
255
+ * For callers that have already rendered a final state — a confirmed
256
+ * deletion, say — where the usual "disable on end" edit would target a
257
+ * message, or a channel, that no longer exists. `onEnd` still runs.
258
+ */
259
+ stop(): void;
260
+ }
261
+
262
+ export function mountInteractiveMessageCollector<S>(
263
+ deps: InteractiveMessageCollectorDeps<S>,
264
+ ): InteractiveMessageHandle {
265
+ const { interaction, response, buttons, view, store, ownerId, idleMs, logger } = deps;
266
+ const selects = deps.selects ?? [];
267
+
268
+ function renderOnto(collected: CollectedMessageInteraction, next: S): InteractionUpdateOptions {
269
+ return toMessageEditOptions(
270
+ view.render(next),
271
+ [...collected.message.attachments.values()],
272
+ next,
273
+ );
274
+ }
275
+
276
+ function createClickContext(clicked: ButtonInteraction): ComponentContext<S> {
277
+ return {
278
+ interaction: clicked,
279
+ state: store.read(),
280
+ update: async (next) => {
281
+ store.write(next);
282
+ await clicked.update(renderOnto(clicked, next));
283
+ },
284
+ };
285
+ }
286
+
287
+ function createPickContext(picked: AnySelectMenuInteraction): SelectComponentContext<S> {
288
+ return {
289
+ interaction: picked,
290
+ values: picked.values,
291
+ state: store.read(),
292
+ update: async (next) => {
293
+ store.write(next);
294
+ await picked.update(renderOnto(picked, next));
295
+ },
296
+ };
297
+ }
298
+
299
+ async function route(collected: CollectedMessageInteraction): Promise<void> {
300
+ if (collected.isButton()) {
301
+ const target = buttons.find((entry) => entry.id === collected.customId);
302
+ await target?.onClick?.(createClickContext(collected));
303
+ return;
304
+ }
305
+ if (collected.isAnySelectMenu()) {
306
+ const target = selects.find((entry) => entry.id === collected.customId);
307
+ await target?.onSelect(createPickContext(collected));
308
+ }
309
+ }
310
+
311
+ async function handleCollected(collected: CollectedMessageInteraction): Promise<void> {
312
+ try {
313
+ await route(collected);
314
+ } catch (error) {
315
+ // At `error`, and with everything the throw carried: whatever lands here
316
+ // left the click unanswered — Discord tells the administrator the
317
+ // interaction failed — so it is a defect to be found, not a hiccup. The
318
+ // custom id says which control it was, since the state that produced it
319
+ // is gone by the time anybody reads this.
320
+ logger.error(
321
+ { err: describeError(error), customId: collected.customId },
322
+ "Failed to update interactive message",
323
+ );
324
+ }
325
+ }
326
+
327
+ function disableOnEnd(): void {
328
+ // The message outlives the collector, so leave the controls visibly inert
329
+ // instead of letting clicks fail silently.
330
+ interaction.editReply(view.disabledControls(store.read())).catch((error) => {
331
+ logger.warn({ err: describeError(error) }, "Failed to disable interactive message controls");
332
+ });
333
+ }
334
+
335
+ /**
336
+ * A caller that stops the message itself has already rendered the final state,
337
+ * so the disabling edit would be at best redundant and at worst aimed at
338
+ * something that is gone. `onEnd` still runs: it answers "the message is over",
339
+ * not "the message timed out".
340
+ */
341
+ let stoppedByCaller = false;
342
+
343
+ async function handleEnd(): Promise<void> {
344
+ if (!stoppedByCaller) {
345
+ disableOnEnd();
346
+ }
347
+ try {
348
+ await deps.onEnd?.();
349
+ } catch (error) {
350
+ logger.warn({ err: describeError(error) }, "Failed to close interactive message");
351
+ }
352
+ }
353
+
354
+ // No `componentType`: the collector takes every message component the view
355
+ // renders — buttons and select menus alike — and `route` sorts them out.
356
+ const collector = response.createMessageComponentCollector({
357
+ // `idle` rearms on every collected interaction, so each click grants a
358
+ // fresh window rather than counting down from the first render.
359
+ idle: idleMs,
360
+ filter: (collected) => collected.user.id === ownerId,
361
+ });
362
+
363
+ collector.on("collect", handleCollected);
364
+ // One `end` listener for both ways out, so nothing an ending owes — inert
365
+ // controls, `onEnd` — depends on which one happened.
366
+ collector.on("end", handleEnd);
367
+
368
+ return {
369
+ stop() {
370
+ stoppedByCaller = true;
371
+ collector.stop();
372
+ },
373
+ };
374
+ }
@@ -0,0 +1,71 @@
1
+ import { type ChatInputCommandInteraction, MessageFlags } from "discord.js";
2
+ import {
3
+ COLLECTOR_IDLE_MS,
4
+ createStateStore,
5
+ type InteractiveMessageHandle,
6
+ type InteractiveSelect,
7
+ type InteractiveView,
8
+ mountInteractiveMessageCollector,
9
+ type StateStore,
10
+ toMessageEditOptions,
11
+ } from "@/discord/components/interactive-message/interactive-message-collector";
12
+ import type { Button } from "@/discord/interaction/button";
13
+ import type { Logger } from "@/logger";
14
+
15
+ export interface InteractiveMessageOptions<S> {
16
+ initialState: S;
17
+ buttons: Button<S>[];
18
+ /** Omitted by a message whose controls are all buttons. */
19
+ selects?: InteractiveSelect<S>[];
20
+ view: InteractiveView<S>;
21
+ ownerId: string;
22
+ /** Run once the message stops reacting, whichever way it ended. */
23
+ onEnd?: () => Promise<void> | void;
24
+ logger: Logger;
25
+ idleMs?: number;
26
+ }
27
+
28
+ /**
29
+ * Send an interactive message and keep its controls live for a bounded time:
30
+ * renders the initial state, then hands the message to the collector.
31
+ *
32
+ * Callers that have already sent their message — or that decide case by case
33
+ * whether it is worth collecting at all — use
34
+ * {@link mountInteractiveMessageCollector} directly.
35
+ *
36
+ * Call it on an already-deferred interaction.
37
+ */
38
+ export async function mountInteractiveMessage<S>(
39
+ interaction: ChatInputCommandInteraction,
40
+ options: InteractiveMessageOptions<S>,
41
+ ): Promise<InteractiveMessageHandle & { readonly store: StateStore<S> }> {
42
+ const store = createStateStore(options.initialState);
43
+ const initial = options.view.render(store.read());
44
+ // A deferred reply carries no attachment yet, so every file the first render
45
+ // states is uploaded — there is nothing up there that could be kept instead.
46
+ const editOptions = toMessageEditOptions(initial, [], store.read());
47
+ const response = await interaction.editReply(
48
+ // `IS_COMPONENTS_V2` is set on the *first* send only, never again: Discord
49
+ // treats it as irreversible on a message — an edit can never clear it —
50
+ // so posing it here and only here is what keeps a later re-render from
51
+ // trying to flip a flag Discord would refuse to change.
52
+ initial.layout === "card"
53
+ ? { ...editOptions, flags: MessageFlags.IsComponentsV2 }
54
+ : editOptions,
55
+ );
56
+
57
+ const handle = mountInteractiveMessageCollector({
58
+ interaction,
59
+ response,
60
+ buttons: options.buttons,
61
+ selects: options.selects,
62
+ view: options.view,
63
+ store,
64
+ ownerId: options.ownerId,
65
+ ...(options.onEnd === undefined ? {} : { onEnd: options.onEnd }),
66
+ idleMs: options.idleMs ?? COLLECTOR_IDLE_MS,
67
+ logger: options.logger,
68
+ });
69
+
70
+ return { ...handle, store };
71
+ }
@@ -0,0 +1,17 @@
1
+ export {
2
+ mountPaginator,
3
+ type PaginatorOptions,
4
+ } from "@/discord/components/paginator/mount-paginator";
5
+ export {
6
+ clampPage,
7
+ createPageState,
8
+ isFirstPage,
9
+ isLastPage,
10
+ nextPage,
11
+ type PageState,
12
+ pageCount,
13
+ previousPage,
14
+ selectPageItems,
15
+ } from "@/discord/components/paginator/page-state";
16
+ export type { PaginatorLabels } from "@/discord/components/paginator/paginator-buttons";
17
+ export type { PaginatorRender } from "@/discord/components/paginator/paginator-view";
@@ -0,0 +1,69 @@
1
+ import type { ChatInputCommandInteraction } from "discord.js";
2
+ import {
3
+ COLLECTOR_IDLE_MS,
4
+ createStateStore,
5
+ mountInteractiveMessageCollector,
6
+ toMessageEditOptions,
7
+ } from "@/discord/components/interactive-message";
8
+ import { createPageState, pageCount } from "@/discord/components/paginator/page-state";
9
+ import {
10
+ createPaginatorButtons,
11
+ type PaginatorLabels,
12
+ resolveLabels,
13
+ } from "@/discord/components/paginator/paginator-buttons";
14
+ import {
15
+ createPaginatorView,
16
+ type PaginatorRender,
17
+ } from "@/discord/components/paginator/paginator-view";
18
+ import type { Logger } from "@/logger";
19
+
20
+ export interface PaginatorOptions<T> {
21
+ items: T[];
22
+ pageSize: number;
23
+ ownerId: string;
24
+ logger: Logger;
25
+ initialPage?: number;
26
+ idleMs?: number;
27
+ labels?: Partial<PaginatorLabels>;
28
+ render: PaginatorRender<T>;
29
+ }
30
+
31
+ /**
32
+ * A composed component: an embed plus previous/next buttons plus a collector.
33
+ * The collector itself is the generic
34
+ * {@link import("@/discord/components/interactive-message").mountInteractiveMessageCollector} —
35
+ * this module only contributes what is specific to pagination: the page state,
36
+ * the navigation buttons and the page view.
37
+ *
38
+ * Call it on an already-deferred interaction.
39
+ */
40
+ export async function mountPaginator<T>(
41
+ interaction: ChatInputCommandInteraction,
42
+ options: PaginatorOptions<T>,
43
+ ): Promise<void> {
44
+ const { items, pageSize, ownerId, render, logger } = options;
45
+ const count = pageCount(items.length, pageSize);
46
+ const store = createStateStore(createPageState(options.initialPage ?? 1, count));
47
+ const buttons = createPaginatorButtons(count, resolveLabels(options.labels));
48
+ const view = createPaginatorView({ items, pageSize, count, render, buttons });
49
+
50
+ const response = await interaction.editReply(toMessageEditOptions(view.render(store.read()), []));
51
+
52
+ // A single page has nothing to navigate to, so it needs no collector at all —
53
+ // a pagination-specific rule, which is why it lives here and not in the
54
+ // generic mechanism.
55
+ if (count <= 1) {
56
+ return;
57
+ }
58
+
59
+ mountInteractiveMessageCollector({
60
+ interaction,
61
+ response,
62
+ buttons,
63
+ view,
64
+ store,
65
+ ownerId,
66
+ idleMs: options.idleMs ?? COLLECTOR_IDLE_MS,
67
+ logger,
68
+ });
69
+ }
@@ -0,0 +1,45 @@
1
+ /** What the paginator remembers between two clicks: the page being displayed (1-based). */
2
+ export interface PageState {
3
+ page: number;
4
+ }
5
+
6
+ /** Total number of pages for a collection, never below 1. */
7
+ export function pageCount(total: number, pageSize: number): number {
8
+ return Math.max(1, Math.ceil(total / pageSize));
9
+ }
10
+
11
+ /** Clamp a page number into the valid `[1, count]` range. */
12
+ export function clampPage(page: number, count: number): number {
13
+ return Math.min(Math.max(page, 1), count);
14
+ }
15
+
16
+ /** Opening state, clamped so a caller-supplied out-of-range page stays valid. */
17
+ export function createPageState(page: number, count: number): PageState {
18
+ return { page: clampPage(page, count) };
19
+ }
20
+
21
+ /** Transition one page back; a no-op on the first page. */
22
+ export function previousPage(state: PageState, count: number): PageState {
23
+ return { page: clampPage(state.page - 1, count) };
24
+ }
25
+
26
+ /** Transition one page forward; a no-op on the last page. */
27
+ export function nextPage(state: PageState, count: number): PageState {
28
+ return { page: clampPage(state.page + 1, count) };
29
+ }
30
+
31
+ /** Whether the "previous" affordance has nothing left to go back to. */
32
+ export function isFirstPage(state: PageState): boolean {
33
+ return state.page <= 1;
34
+ }
35
+
36
+ /** Whether the "next" affordance has nothing left to go forward to. */
37
+ export function isLastPage(state: PageState, count: number): boolean {
38
+ return state.page >= count;
39
+ }
40
+
41
+ /** Slice of the collection visible on the current page. */
42
+ export function selectPageItems<T>(items: T[], state: PageState, pageSize: number): T[] {
43
+ const start = (state.page - 1) * pageSize;
44
+ return items.slice(start, start + pageSize);
45
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ isFirstPage,
3
+ isLastPage,
4
+ nextPage,
5
+ type PageState,
6
+ previousPage,
7
+ } from "@/discord/components/paginator/page-state";
8
+ import { type Button, createButton } from "@/discord/interaction/button";
9
+
10
+ /** Custom IDs of the two navigation buttons; also what the collector routes on. */
11
+ const PREV_ID = "paginator:prev";
12
+ const NEXT_ID = "paginator:next";
13
+
14
+ export interface PaginatorLabels {
15
+ previous: string;
16
+ next: string;
17
+ previousEmoji?: string;
18
+ nextEmoji?: string;
19
+ }
20
+
21
+ const DEFAULT_LABELS: PaginatorLabels = {
22
+ previous: "Previous",
23
+ next: "Next",
24
+ previousEmoji: "◀️",
25
+ nextEmoji: "▶️",
26
+ };
27
+
28
+ /** Complete a caller's partial labels with the defaults. */
29
+ export function resolveLabels(labels?: Partial<PaginatorLabels>): PaginatorLabels {
30
+ return { ...DEFAULT_LABELS, ...labels };
31
+ }
32
+
33
+ /**
34
+ * The navigation buttons, bound to a fixed page count. Each button carries its
35
+ * own disabled predicate and transition, so the collector only has to hand a
36
+ * click to the matching button instead of knowing what the click means.
37
+ */
38
+ export function createPaginatorButtons(
39
+ count: number,
40
+ labels: PaginatorLabels,
41
+ ): Button<PageState>[] {
42
+ return [
43
+ createButton<PageState>({
44
+ id: PREV_ID,
45
+ label: labels.previous,
46
+ emoji: labels.previousEmoji,
47
+ style: "secondary",
48
+ disabled: (current) => isFirstPage(current),
49
+ onClick: (ctx) => ctx.update(previousPage(ctx.state, count)),
50
+ }),
51
+ createButton<PageState>({
52
+ id: NEXT_ID,
53
+ label: labels.next,
54
+ emoji: labels.nextEmoji,
55
+ style: "secondary",
56
+ disabled: (current) => isLastPage(current, count),
57
+ onClick: (ctx) => ctx.update(nextPage(ctx.state, count)),
58
+ }),
59
+ ];
60
+ }