chapterjs 0.1.2 → 0.1.4

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 (320) hide show
  1. package/dist/assets/asset.d.ts +46 -0
  2. package/dist/assets/asset.js +86 -0
  3. package/dist/assets/public.d.ts +19 -0
  4. package/dist/assets/public.js +120 -0
  5. package/dist/cache/cache.d.ts +62 -0
  6. package/dist/cache/cache.js +80 -0
  7. package/dist/cache/store.d.ts +45 -0
  8. package/dist/cache/store.js +53 -0
  9. package/dist/cli/build.d.ts +41 -0
  10. package/dist/cli/build.js +206 -0
  11. package/dist/cli/cli.js +108 -25
  12. package/dist/cli/cluster.d.ts +70 -0
  13. package/dist/cli/cluster.js +213 -0
  14. package/dist/cli/dev.d.ts +18 -1
  15. package/dist/cli/dev.js +252 -198
  16. package/dist/cli/env.d.ts +31 -0
  17. package/dist/cli/env.js +93 -0
  18. package/dist/cli/log.d.ts +13 -0
  19. package/dist/cli/log.js +17 -0
  20. package/dist/cli/node-version.d.ts +8 -0
  21. package/dist/cli/node-version.js +13 -0
  22. package/dist/cli/preflight.d.ts +36 -0
  23. package/dist/cli/preflight.js +122 -0
  24. package/dist/cli/project.d.ts +122 -0
  25. package/dist/cli/project.js +452 -0
  26. package/dist/cli/start.d.ts +25 -0
  27. package/dist/cli/start.js +225 -0
  28. package/dist/commands/autocomplete.d.ts +13 -0
  29. package/dist/commands/autocomplete.js +159 -0
  30. package/dist/commands/command.d.ts +346 -0
  31. package/dist/commands/command.js +28 -0
  32. package/dist/commands/convention.d.ts +64 -0
  33. package/dist/commands/convention.js +464 -0
  34. package/dist/commands/register.d.ts +18 -0
  35. package/dist/commands/register.js +42 -0
  36. package/dist/commands/router.d.ts +20 -0
  37. package/dist/commands/router.js +228 -0
  38. package/dist/commands/tree.d.ts +43 -0
  39. package/dist/commands/tree.js +206 -0
  40. package/dist/commands/twins.d.ts +15 -0
  41. package/dist/commands/twins.js +78 -0
  42. package/dist/components/button.d.ts +79 -0
  43. package/dist/components/button.js +108 -0
  44. package/dist/components/component.d.ts +140 -0
  45. package/dist/components/component.js +39 -0
  46. package/dist/components/convention.d.ts +35 -0
  47. package/dist/components/convention.js +339 -0
  48. package/dist/components/custom-id.d.ts +72 -0
  49. package/dist/components/custom-id.js +128 -0
  50. package/dist/components/embed.d.ts +28 -0
  51. package/dist/components/embed.js +11 -0
  52. package/dist/components/file.d.ts +30 -0
  53. package/dist/components/file.js +52 -0
  54. package/dist/components/instance.d.ts +100 -0
  55. package/dist/components/instance.js +86 -0
  56. package/dist/components/layout.d.ts +103 -0
  57. package/dist/components/layout.js +279 -0
  58. package/dist/components/modal.d.ts +244 -0
  59. package/dist/components/modal.js +399 -0
  60. package/dist/components/options.d.ts +41 -0
  61. package/dist/components/options.js +101 -0
  62. package/dist/components/render.d.ts +12 -0
  63. package/dist/components/render.js +58 -0
  64. package/dist/components/router.d.ts +18 -0
  65. package/dist/components/router.js +256 -0
  66. package/dist/components/select.d.ts +144 -0
  67. package/dist/components/select.js +179 -0
  68. package/dist/core/bot.d.ts +99 -0
  69. package/dist/core/bot.js +174 -0
  70. package/dist/core/memory.d.ts +66 -0
  71. package/dist/core/memory.js +115 -0
  72. package/dist/discord/api.d.ts +112 -0
  73. package/dist/discord/api.js +115 -0
  74. package/dist/discord/cdn.d.ts +67 -0
  75. package/dist/discord/cdn.js +100 -0
  76. package/dist/discord/codes.d.ts +171 -0
  77. package/dist/discord/codes.js +421 -0
  78. package/dist/discord/endpoint.d.ts +56 -0
  79. package/dist/discord/endpoint.js +53 -0
  80. package/dist/discord/endpoints.d.ts +1529 -0
  81. package/dist/discord/endpoints.js +1139 -0
  82. package/dist/discord/formatting.d.ts +67 -0
  83. package/dist/discord/formatting.js +87 -0
  84. package/dist/discord/index.d.ts +10 -0
  85. package/dist/discord/index.js +13 -0
  86. package/dist/discord/intents.d.ts +48 -0
  87. package/dist/discord/intents.js +128 -0
  88. package/dist/discord/permissions.d.ts +94 -0
  89. package/dist/discord/permissions.js +185 -0
  90. package/dist/discord/snowflake.d.ts +21 -0
  91. package/dist/discord/snowflake.js +36 -0
  92. package/dist/discord/types/application-command.d.ts +332 -0
  93. package/dist/discord/types/application-command.js +48 -0
  94. package/dist/discord/types/application-role-connection-metadata.d.ts +42 -0
  95. package/dist/discord/types/application-role-connection-metadata.js +22 -0
  96. package/dist/discord/types/application.d.ts +208 -0
  97. package/dist/discord/types/application.js +48 -0
  98. package/dist/discord/types/audit-log.d.ts +255 -0
  99. package/dist/discord/types/audit-log.js +144 -0
  100. package/dist/discord/types/auto-moderation.d.ts +167 -0
  101. package/dist/discord/types/auto-moderation.js +52 -0
  102. package/dist/discord/types/channel.d.ts +557 -0
  103. package/dist/discord/types/channel.js +88 -0
  104. package/dist/discord/types/common.d.ts +54 -0
  105. package/dist/discord/types/common.js +38 -0
  106. package/dist/discord/types/component.d.ts +775 -0
  107. package/dist/discord/types/component.js +76 -0
  108. package/dist/discord/types/emoji.d.ts +74 -0
  109. package/dist/discord/types/emoji.js +1 -0
  110. package/dist/discord/types/entitlement.d.ts +84 -0
  111. package/dist/discord/types/entitlement.js +22 -0
  112. package/dist/discord/types/gateway-events.d.ts +1148 -0
  113. package/dist/discord/types/gateway-events.js +54 -0
  114. package/dist/discord/types/gateway.d.ts +38 -0
  115. package/dist/discord/types/gateway.js +1 -0
  116. package/dist/discord/types/guild-scheduled-event.d.ts +257 -0
  117. package/dist/discord/types/guild-scheduled-event.js +68 -0
  118. package/dist/discord/types/guild-template.d.ts +51 -0
  119. package/dist/discord/types/guild-template.js +1 -0
  120. package/dist/discord/types/guild.d.ts +910 -0
  121. package/dist/discord/types/guild.js +153 -0
  122. package/dist/discord/types/index.d.ts +31 -0
  123. package/dist/discord/types/index.js +31 -0
  124. package/dist/discord/types/interaction.d.ts +321 -0
  125. package/dist/discord/types/interaction.js +47 -0
  126. package/dist/discord/types/invite.d.ts +158 -0
  127. package/dist/discord/types/invite.js +39 -0
  128. package/dist/discord/types/message.d.ts +834 -0
  129. package/dist/discord/types/message.js +159 -0
  130. package/dist/discord/types/oauth2.d.ts +22 -0
  131. package/dist/discord/types/oauth2.js +1 -0
  132. package/dist/discord/types/permissions.d.ts +183 -0
  133. package/dist/discord/types/permissions.js +118 -0
  134. package/dist/discord/types/poll.d.ts +106 -0
  135. package/dist/discord/types/poll.js +8 -0
  136. package/dist/discord/types/sku.d.ts +47 -0
  137. package/dist/discord/types/sku.js +26 -0
  138. package/dist/discord/types/soundboard.d.ts +71 -0
  139. package/dist/discord/types/soundboard.js +1 -0
  140. package/dist/discord/types/stage-instance.d.ts +58 -0
  141. package/dist/discord/types/stage-instance.js +10 -0
  142. package/dist/discord/types/sticker.d.ts +115 -0
  143. package/dist/discord/types/sticker.js +20 -0
  144. package/dist/discord/types/subscription.d.ts +54 -0
  145. package/dist/discord/types/subscription.js +12 -0
  146. package/dist/discord/types/team.d.ts +55 -0
  147. package/dist/discord/types/team.js +21 -0
  148. package/dist/discord/types/user.d.ts +273 -0
  149. package/dist/discord/types/user.js +83 -0
  150. package/dist/discord/types/voice.d.ts +72 -0
  151. package/dist/discord/types/voice.js +1 -0
  152. package/dist/discord/types/webhook.d.ts +184 -0
  153. package/dist/discord/types/webhook.js +12 -0
  154. package/dist/events/convention.d.ts +25 -0
  155. package/dist/events/convention.js +97 -0
  156. package/dist/events/event.d.ts +8 -0
  157. package/dist/events/event.js +20 -0
  158. package/dist/events/registry.d.ts +706 -0
  159. package/dist/events/registry.js +839 -0
  160. package/dist/events/router.d.ts +45 -0
  161. package/dist/events/router.js +133 -0
  162. package/dist/events/types.d.ts +7 -0
  163. package/dist/events/types.js +48 -0
  164. package/dist/gateway/errors.d.ts +18 -0
  165. package/dist/gateway/errors.js +47 -0
  166. package/dist/gateway/gateway.d.ts +64 -0
  167. package/dist/gateway/gateway.js +117 -0
  168. package/dist/gateway/identify-queue.d.ts +25 -0
  169. package/dist/gateway/identify-queue.js +29 -0
  170. package/dist/gateway/shard.d.ts +94 -0
  171. package/dist/gateway/shard.js +414 -0
  172. package/dist/gateway/state.d.ts +11 -0
  173. package/dist/gateway/state.js +184 -0
  174. package/dist/index.d.ts +55 -18
  175. package/dist/index.js +38 -12
  176. package/dist/interactions/cooldown.d.ts +33 -0
  177. package/dist/interactions/cooldown.js +74 -0
  178. package/dist/interactions/dispatch.d.ts +91 -0
  179. package/dist/interactions/dispatch.js +110 -0
  180. package/dist/loader/generated.d.ts +37 -0
  181. package/dist/loader/generated.js +114 -0
  182. package/dist/loader/hot.d.ts +13 -0
  183. package/dist/loader/hot.js +85 -0
  184. package/dist/loader/loader.d.ts +72 -0
  185. package/dist/loader/loader.js +135 -0
  186. package/dist/loader/locate.d.ts +10 -0
  187. package/dist/loader/locate.js +36 -0
  188. package/dist/loader/watch.d.ts +17 -0
  189. package/dist/loader/watch.js +66 -0
  190. package/dist/main.d.ts +3 -0
  191. package/dist/main.js +6 -0
  192. package/dist/messages/commands.d.ts +23 -0
  193. package/dist/messages/commands.js +123 -0
  194. package/dist/messages/convention.d.ts +89 -0
  195. package/dist/messages/convention.js +347 -0
  196. package/dist/messages/messages.d.ts +219 -0
  197. package/dist/messages/messages.js +30 -0
  198. package/dist/messages/phrases.d.ts +23 -0
  199. package/dist/messages/phrases.js +44 -0
  200. package/dist/messages/translate.d.ts +100 -0
  201. package/dist/messages/translate.js +249 -0
  202. package/dist/presence/convention.d.ts +26 -0
  203. package/dist/presence/convention.js +133 -0
  204. package/dist/presence/presence.d.ts +57 -0
  205. package/dist/presence/presence.js +27 -0
  206. package/dist/rest/errors.d.ts +55 -0
  207. package/dist/rest/errors.js +92 -0
  208. package/dist/rest/rate-limiter.d.ts +46 -0
  209. package/dist/rest/rate-limiter.js +153 -0
  210. package/dist/rest/rest.d.ts +63 -0
  211. package/dist/rest/rest.js +194 -0
  212. package/dist/structures/base.d.ts +48 -0
  213. package/dist/structures/base.js +84 -0
  214. package/dist/structures/channel.d.ts +380 -0
  215. package/dist/structures/channel.js +627 -0
  216. package/dist/structures/context.d.ts +30 -0
  217. package/dist/structures/context.js +7 -0
  218. package/dist/structures/emoji.d.ts +46 -0
  219. package/dist/structures/emoji.js +65 -0
  220. package/dist/structures/entities.d.ts +62 -0
  221. package/dist/structures/entities.js +226 -0
  222. package/dist/structures/guild.d.ts +348 -0
  223. package/dist/structures/guild.js +466 -0
  224. package/dist/structures/interaction.d.ts +197 -0
  225. package/dist/structures/interaction.js +396 -0
  226. package/dist/structures/invite.d.ts +38 -0
  227. package/dist/structures/invite.js +57 -0
  228. package/dist/structures/known.d.ts +28 -0
  229. package/dist/structures/known.js +58 -0
  230. package/dist/structures/member.d.ts +140 -0
  231. package/dist/structures/member.js +260 -0
  232. package/dist/structures/message.d.ts +197 -0
  233. package/dist/structures/message.js +268 -0
  234. package/dist/structures/payload.d.ts +91 -0
  235. package/dist/structures/payload.js +184 -0
  236. package/dist/structures/role.d.ts +69 -0
  237. package/dist/structures/role.js +112 -0
  238. package/dist/structures/user.d.ts +59 -0
  239. package/dist/structures/user.js +94 -0
  240. package/dist/structures/webhook.d.ts +46 -0
  241. package/dist/structures/webhook.js +79 -0
  242. package/dist/tasks/convention.d.ts +26 -0
  243. package/dist/tasks/convention.js +78 -0
  244. package/dist/tasks/schedule.d.ts +54 -0
  245. package/dist/tasks/schedule.js +236 -0
  246. package/dist/tasks/scheduler.d.ts +33 -0
  247. package/dist/tasks/scheduler.js +107 -0
  248. package/dist/tasks/task.d.ts +76 -0
  249. package/dist/tasks/task.js +28 -0
  250. package/dist/util/case.d.ts +13 -0
  251. package/dist/util/case.js +54 -0
  252. package/dist/util/duration.d.ts +8 -0
  253. package/dist/util/duration.js +29 -0
  254. package/dist/util/websocket.d.ts +9 -0
  255. package/dist/util/websocket.js +31 -0
  256. package/dist/voice/connection.d.ts +90 -0
  257. package/dist/voice/connection.js +494 -0
  258. package/dist/voice/dave.d.ts +109 -0
  259. package/dist/voice/dave.js +245 -0
  260. package/dist/voice/opus.d.ts +19 -0
  261. package/dist/voice/opus.js +273 -0
  262. package/dist/voice/session.d.ts +55 -0
  263. package/dist/voice/session.js +405 -0
  264. package/dist/voice/source.d.ts +22 -0
  265. package/dist/voice/source.js +195 -0
  266. package/dist/voice/transport.d.ts +43 -0
  267. package/dist/voice/transport.js +140 -0
  268. package/dist/voice/usage.d.ts +4 -0
  269. package/dist/voice/usage.js +33 -0
  270. package/package.json +26 -7
  271. package/vendor/dave/LICENSE +231 -0
  272. package/vendor/dave/libdave.d.mts +99 -0
  273. package/vendor/dave/libdave.mjs +2 -0
  274. package/vendor/dave/libdave.wasm +0 -0
  275. package/dist/components.d.ts +0 -97
  276. package/dist/components.js +0 -260
  277. package/dist/context.d.ts +0 -72
  278. package/dist/context.js +0 -273
  279. package/dist/discord/client.d.ts +0 -35
  280. package/dist/discord/client.js +0 -66
  281. package/dist/discord/constants.d.ts +0 -49
  282. package/dist/discord/constants.js +0 -61
  283. package/dist/discord/events.d.ts +0 -129
  284. package/dist/discord/events.js +0 -102
  285. package/dist/discord/gateway.d.ts +0 -61
  286. package/dist/discord/gateway.js +0 -320
  287. package/dist/discord/payloads.d.ts +0 -275
  288. package/dist/discord/payloads.js +0 -3
  289. package/dist/discord/presence.d.ts +0 -42
  290. package/dist/discord/presence.js +0 -51
  291. package/dist/discord/rest.d.ts +0 -49
  292. package/dist/discord/rest.js +0 -271
  293. package/dist/discord/types.d.ts +0 -334
  294. package/dist/discord/types.js +0 -4
  295. package/dist/events.d.ts +0 -94
  296. package/dist/events.js +0 -255
  297. package/dist/presence.d.ts +0 -92
  298. package/dist/presence.js +0 -250
  299. package/dist/structures/Channel.d.ts +0 -110
  300. package/dist/structures/Channel.js +0 -171
  301. package/dist/structures/Guild.d.ts +0 -70
  302. package/dist/structures/Guild.js +0 -105
  303. package/dist/structures/Interaction.d.ts +0 -113
  304. package/dist/structures/Interaction.js +0 -274
  305. package/dist/structures/Member.d.ts +0 -70
  306. package/dist/structures/Member.js +0 -124
  307. package/dist/structures/Message.d.ts +0 -87
  308. package/dist/structures/Message.js +0 -182
  309. package/dist/structures/Reaction.d.ts +0 -38
  310. package/dist/structures/Reaction.js +0 -64
  311. package/dist/structures/Role.d.ts +0 -43
  312. package/dist/structures/Role.js +0 -77
  313. package/dist/structures/User.d.ts +0 -41
  314. package/dist/structures/User.js +0 -79
  315. package/dist/structures/utils.d.ts +0 -72
  316. package/dist/structures/utils.js +0 -59
  317. package/dist/utils/errors.d.ts +0 -5
  318. package/dist/utils/errors.js +0 -28
  319. package/dist/utils/modules.d.ts +0 -39
  320. package/dist/utils/modules.js +0 -156
@@ -0,0 +1,236 @@
1
+ // When a task runs: a plain duration (`'10m'`) or a cron expression, with
2
+ // a timezone. Written once, with no dependency: the next occurrence of a
3
+ // cron is found by reading the clock of the timezone through `Intl`.
4
+ import { DURATION_EXAMPLE, parseDuration } from '../util/duration.js';
5
+ const fail = (message) => {
6
+ throw new TypeError(message);
7
+ };
8
+ /** The shortest `every` accepted, in milliseconds. */
9
+ export const MIN_EVERY = 1000;
10
+ /**
11
+ * Parses a duration: a number and a unit, `s`, `m`, `h` or `d`, like
12
+ * `'30s'`, `'10m'`, `'2h'`, `'1d'`. Several parts add up: `'1h30m'`.
13
+ */
14
+ export function parseEvery(text) {
15
+ const example = `every: '10m' (${DURATION_EXAMPLE})`;
16
+ if (typeof text !== 'string' || text.trim() === '') {
17
+ return fail(`"every" says how often the task runs: ${example}.`);
18
+ }
19
+ const ms = parseDuration(text);
20
+ if (ms === null) {
21
+ return fail(`"every" is ${JSON.stringify(text)}, which is not a duration: ${example}.`);
22
+ }
23
+ if (ms < MIN_EVERY) {
24
+ fail(`"every" is ${JSON.stringify(text)}: a task runs at most once per second.`);
25
+ }
26
+ return { kind: 'every', text, ms };
27
+ }
28
+ const NAMES = {
29
+ months: {
30
+ jan: 1,
31
+ feb: 2,
32
+ mar: 3,
33
+ apr: 4,
34
+ may: 5,
35
+ jun: 6,
36
+ jul: 7,
37
+ aug: 8,
38
+ sep: 9,
39
+ oct: 10,
40
+ nov: 11,
41
+ dec: 12,
42
+ },
43
+ weekdays: { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 },
44
+ };
45
+ /** The values one field of a cron matches. */
46
+ function parseField(text, field, low, high, expression) {
47
+ const values = new Set();
48
+ const names = NAMES[field] ?? {};
49
+ const read = (part) => {
50
+ const named = names[part.toLowerCase()];
51
+ if (named !== undefined)
52
+ return named;
53
+ if (!/^\d+$/.test(part)) {
54
+ return fail(`In cron ${JSON.stringify(expression)}, ${JSON.stringify(part)} is not a value for ${field}: use numbers from ${low} to ${high}${Object.keys(names).length ? `, or ${Object.keys(names).join(', ')}` : ''}.`);
55
+ }
56
+ const value = Number(part);
57
+ if (value < low || value > high) {
58
+ fail(`In cron ${JSON.stringify(expression)}, ${part} is out of range for ${field}: ${low} to ${high}.`);
59
+ }
60
+ return value;
61
+ };
62
+ for (const item of text.split(',')) {
63
+ const [range = '', stepText] = item.split('/');
64
+ const step = stepText === undefined ? 1 : Number(stepText);
65
+ if (stepText !== undefined && (!/^\d+$/.test(stepText) || step < 1)) {
66
+ fail(`In cron ${JSON.stringify(expression)}, "/${stepText}" is not a step: write a number, like */15.`);
67
+ }
68
+ let from;
69
+ let to;
70
+ if (range === '*') {
71
+ from = low;
72
+ to = high;
73
+ }
74
+ else if (range.includes('-')) {
75
+ const [a = '', b = ''] = range.split('-');
76
+ from = read(a);
77
+ to = read(b);
78
+ if (from > to) {
79
+ fail(`In cron ${JSON.stringify(expression)}, the range ${range} goes backwards.`);
80
+ }
81
+ }
82
+ else {
83
+ from = read(range);
84
+ to = stepText === undefined ? from : high;
85
+ }
86
+ for (let value = from; value <= to; value += step)
87
+ values.add(value);
88
+ }
89
+ // Sunday is 0 and 7.
90
+ if (field === 'weekdays' && values.has(7)) {
91
+ values.delete(7);
92
+ values.add(0);
93
+ }
94
+ return values;
95
+ }
96
+ /** Whether a timezone exists, as `Intl` knows them. */
97
+ export function isTimezone(name) {
98
+ try {
99
+ new Intl.DateTimeFormat('en-US', { timeZone: name });
100
+ return true;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ }
106
+ /** The timezone of the machine. */
107
+ export const localTimezone = () => Intl.DateTimeFormat().resolvedOptions().timeZone;
108
+ /**
109
+ * Parses a cron expression: five fields, minute, hour, day of month,
110
+ * month and day of week, with `*`, lists, ranges, steps and names.
111
+ * @see https://en.wikipedia.org/wiki/Cron#Cron_expression
112
+ */
113
+ export function parseCron(text, timezone) {
114
+ const example = `cron: '0 9 * * 1-5' (minute, hour, day of month, month, day of week)`;
115
+ if (typeof text !== 'string' || text.trim() === '') {
116
+ return fail(`"cron" says when the task runs: ${example}.`);
117
+ }
118
+ const fields = text.trim().split(/\s+/);
119
+ if (fields.length !== 5) {
120
+ return fail(`"cron" is ${JSON.stringify(text)}, which has ${fields.length} fields: a cron has 5, ${example}.`);
121
+ }
122
+ if (timezone !== undefined &&
123
+ (typeof timezone !== 'string' || !isTimezone(timezone))) {
124
+ fail(`"timezone" is ${JSON.stringify(timezone) ?? typeof timezone}, which is not a timezone. Write one like 'Europe/Paris' or 'America/New_York'; without it, the task follows the clock of the machine (${localTimezone()}).`);
125
+ }
126
+ const [minute, hour, day, month, weekday] = fields;
127
+ const schedule = {
128
+ kind: 'cron',
129
+ text: text.trim(),
130
+ timezone: timezone ?? localTimezone(),
131
+ minutes: parseField(minute, 'minutes', 0, 59, text),
132
+ hours: parseField(hour, 'hours', 0, 23, text),
133
+ days: parseField(day, 'days', 1, 31, text),
134
+ months: parseField(month, 'months', 1, 12, text),
135
+ weekdays: parseField(weekday, 'weekdays', 0, 7, text),
136
+ anyDay: day === '*',
137
+ anyWeekday: weekday === '*',
138
+ };
139
+ if (!nextCron(schedule, new Date())) {
140
+ fail(`"cron" is ${JSON.stringify(text)}, which never happens: no day of the next 5 years matches it.`);
141
+ }
142
+ return schedule;
143
+ }
144
+ const formatters = new Map();
145
+ const WEEKDAYS = NAMES.weekdays;
146
+ /** What the clock of a timezone shows at a moment. */
147
+ function clockOf(date, timezone) {
148
+ let formatter = formatters.get(timezone);
149
+ if (!formatter) {
150
+ formatter = new Intl.DateTimeFormat('en-US', {
151
+ timeZone: timezone,
152
+ hourCycle: 'h23',
153
+ month: 'numeric',
154
+ day: 'numeric',
155
+ hour: 'numeric',
156
+ minute: 'numeric',
157
+ weekday: 'short',
158
+ });
159
+ formatters.set(timezone, formatter);
160
+ }
161
+ const clock = { minute: 0, hour: 0, day: 0, month: 0, weekday: 0 };
162
+ for (const part of formatter.formatToParts(date)) {
163
+ if (part.type === 'weekday')
164
+ clock.weekday = WEEKDAYS[part.value.toLowerCase()];
165
+ else if (part.type in clock)
166
+ clock[part.type] = Number(part.value);
167
+ }
168
+ return clock;
169
+ }
170
+ const MINUTE = 60_000;
171
+ const HOUR = 3_600_000;
172
+ const FIVE_YEARS = 5 * 366 * 86_400_000;
173
+ /**
174
+ * The next moment a cron matches, strictly after `from`; `null` when none
175
+ * comes in the next five years. Walks the clock of the timezone: minute
176
+ * by minute within a matching hour, hour by hour otherwise, so a change
177
+ * of daylight saving time is read, not computed.
178
+ */
179
+ export function nextCron(schedule, from) {
180
+ // Day of month and day of week: either when both are given (as cron does).
181
+ const dayMatches = (clock) => {
182
+ const byDay = schedule.days.has(clock.day);
183
+ const byWeekday = schedule.weekdays.has(clock.weekday);
184
+ if (schedule.anyDay && schedule.anyWeekday)
185
+ return true;
186
+ if (schedule.anyDay)
187
+ return byWeekday;
188
+ if (schedule.anyWeekday)
189
+ return byDay;
190
+ return byDay || byWeekday;
191
+ };
192
+ // When clocks go back, a time of the day comes twice: a task that ran
193
+ // at the first one does not run again at the second.
194
+ const last = clockOf(from, schedule.timezone);
195
+ const sameTime = (clock) => clock.month === last.month &&
196
+ clock.day === last.day &&
197
+ clock.hour === last.hour &&
198
+ clock.minute === last.minute;
199
+ let at = Math.floor(from.getTime() / MINUTE) * MINUTE + MINUTE;
200
+ const limit = from.getTime() + FIVE_YEARS;
201
+ while (at <= limit) {
202
+ const clock = clockOf(new Date(at), schedule.timezone);
203
+ if (!schedule.months.has(clock.month) || !dayMatches(clock)) {
204
+ // Not today: to the next hour, then look again.
205
+ at += HOUR - clock.minute * MINUTE;
206
+ }
207
+ else if (!schedule.hours.has(clock.hour)) {
208
+ at += HOUR - clock.minute * MINUTE;
209
+ }
210
+ else if (!schedule.minutes.has(clock.minute) || sameTime(clock)) {
211
+ at += MINUTE;
212
+ }
213
+ else {
214
+ return new Date(at);
215
+ }
216
+ }
217
+ return null;
218
+ }
219
+ /** The next time a schedule is due, strictly after `from`. */
220
+ export function nextRun(schedule, from) {
221
+ if (schedule.kind === 'every')
222
+ return new Date(from.getTime() + schedule.ms);
223
+ const next = nextCron(schedule, from);
224
+ if (!next) {
225
+ throw new Error(`The cron ${JSON.stringify(schedule.text)} never happens again.`);
226
+ }
227
+ return next;
228
+ }
229
+ /** What tells one schedule from another: the same key is the same timing. */
230
+ export const scheduleKey = (schedule) => schedule.kind === 'every'
231
+ ? `every ${schedule.ms}`
232
+ : `cron ${schedule.text} ${schedule.timezone}`;
233
+ /** The schedule in words, for the developer. */
234
+ export const describeSchedule = (schedule) => schedule.kind === 'every'
235
+ ? `every ${schedule.text}`
236
+ : `at "${schedule.text}" (${schedule.timezone})`;
@@ -0,0 +1,33 @@
1
+ import type { TaskContext } from './task.js';
2
+ import type { LoadedTask } from './convention.js';
3
+ /** A task, with the file it comes from (to report its errors). */
4
+ export interface TaskEntry {
5
+ file: string;
6
+ task: LoadedTask;
7
+ }
8
+ /** What runs the tasks. */
9
+ export interface Scheduler {
10
+ /**
11
+ * Replaces the tasks (after a load or a reload). A task whose timing did
12
+ * not change keeps its next time; a new one is counted from now.
13
+ */
14
+ set(entries: Iterable<TaskEntry>): void;
15
+ /** Starts running the tasks that are due, with what `context` gives. */
16
+ start(context: () => Omit<TaskContext, 'now'> | null): void;
17
+ /** Stops everything; nothing runs after this. */
18
+ stop(): void;
19
+ }
20
+ export interface SchedulerOptions {
21
+ /** A `run` threw: reported with the file it comes from. */
22
+ onError: (file: string, error: unknown) => void;
23
+ /** Something the developer should know, that is not an error of theirs. */
24
+ onWarning: (file: string, message: string) => void;
25
+ }
26
+ /** Tasks run by the timers of this process. */
27
+ export declare class TimerScheduler implements Scheduler {
28
+ #private;
29
+ constructor(options: SchedulerOptions);
30
+ set(entries: Iterable<TaskEntry>): void;
31
+ start(context: () => Omit<TaskContext, 'now'> | null): void;
32
+ stop(): void;
33
+ }
@@ -0,0 +1,107 @@
1
+ // Runs the tasks of the project when they are due. Behind an interface:
2
+ // this one keeps timers in the process; a bot on several machines would
3
+ // share one through another implementation, with no change to the files.
4
+ import { describeSchedule, nextRun, scheduleKey } from './schedule.js';
5
+ /** The longest a timer of Node can wait. */
6
+ const MAX_DELAY = 2 ** 31 - 1;
7
+ /** Tasks run by the timers of this process. */
8
+ export class TimerScheduler {
9
+ #options;
10
+ #tasks = new Map();
11
+ #context = null;
12
+ constructor(options) {
13
+ this.#options = options;
14
+ }
15
+ set(entries) {
16
+ const kept = new Map();
17
+ const started = [];
18
+ for (const entry of entries) {
19
+ const key = scheduleKey(entry.task.schedule);
20
+ const previous = this.#tasks.get(entry.file);
21
+ if (previous && previous.key === key) {
22
+ // Same timing: the next time stays, only the code changes.
23
+ previous.entry = entry;
24
+ kept.set(entry.file, previous);
25
+ this.#tasks.delete(entry.file);
26
+ continue;
27
+ }
28
+ const scheduled = {
29
+ entry,
30
+ key,
31
+ next: nextRun(entry.task.schedule, new Date()),
32
+ timer: null,
33
+ running: false,
34
+ };
35
+ kept.set(entry.file, scheduled);
36
+ started.push(scheduled);
37
+ }
38
+ // What is left was removed, or changed timing: it starts over.
39
+ for (const old of this.#tasks.values()) {
40
+ if (old.timer)
41
+ clearTimeout(old.timer);
42
+ }
43
+ this.#tasks = kept;
44
+ if (this.#context) {
45
+ for (const scheduled of started) {
46
+ this.#arm(scheduled);
47
+ if (scheduled.entry.task.onStart)
48
+ this.#run(scheduled, new Date());
49
+ }
50
+ }
51
+ }
52
+ start(context) {
53
+ if (this.#context)
54
+ return;
55
+ this.#context = context;
56
+ const now = new Date();
57
+ for (const scheduled of this.#tasks.values()) {
58
+ scheduled.next = nextRun(scheduled.entry.task.schedule, now);
59
+ this.#arm(scheduled);
60
+ if (scheduled.entry.task.onStart)
61
+ this.#run(scheduled, now);
62
+ }
63
+ }
64
+ stop() {
65
+ this.#context = null;
66
+ for (const scheduled of this.#tasks.values()) {
67
+ if (scheduled.timer)
68
+ clearTimeout(scheduled.timer);
69
+ scheduled.timer = null;
70
+ }
71
+ }
72
+ /** Waits for the next time of a task, in steps a timer can hold. */
73
+ #arm(scheduled) {
74
+ if (scheduled.timer)
75
+ clearTimeout(scheduled.timer);
76
+ const delay = scheduled.next.getTime() - Date.now();
77
+ scheduled.timer = setTimeout(() => {
78
+ if (!this.#context)
79
+ return;
80
+ if (Date.now() < scheduled.next.getTime())
81
+ return this.#arm(scheduled);
82
+ const due = scheduled.next;
83
+ scheduled.next = nextRun(scheduled.entry.task.schedule, due);
84
+ this.#arm(scheduled);
85
+ this.#run(scheduled, due);
86
+ }, Math.min(Math.max(delay, 0), MAX_DELAY));
87
+ }
88
+ /** Runs a task once, unless it is still running from the last time. */
89
+ #run(scheduled, now) {
90
+ const { file, task } = scheduled.entry;
91
+ if (scheduled.running) {
92
+ this.#options.onWarning(file, `${task.name} was still running when its next time came (${describeSchedule(task.schedule)}): that run was skipped.`);
93
+ return;
94
+ }
95
+ const context = this.#context?.();
96
+ if (!context) {
97
+ this.#options.onWarning(file, `${task.name} was due while the bot was not connected: that run was skipped.`);
98
+ return;
99
+ }
100
+ scheduled.running = true;
101
+ new Promise(resolve => resolve(scheduled.entry.task.run(Object.freeze({ ...context, now }))))
102
+ .catch((error) => this.#options.onError(file, error))
103
+ .finally(() => {
104
+ scheduled.running = false;
105
+ });
106
+ }
107
+ }
@@ -0,0 +1,76 @@
1
+ import type { Snowflake } from '../discord/types/common.js';
2
+ import type { Guild } from '../structures/guild.js';
3
+ import type { User } from '../structures/user.js';
4
+ import type { TranslationContext } from '../messages/messages.js';
5
+ /** What `run` receives: the bot, as the `ready` event gives it, and the time. */
6
+ export interface TaskContext {
7
+ /** The bot itself. */
8
+ user: User;
9
+ /** The servers the bot is in. */
10
+ guilds: ReadonlyMap<Snowflake, Guild>;
11
+ /** When this run was due. */
12
+ now: Date;
13
+ }
14
+ interface TaskConfigBase {
15
+ /**
16
+ * Also run once as soon as the bot is connected, without waiting for the
17
+ * first time of the schedule.
18
+ */
19
+ onStart?: boolean;
20
+ /** What the task does. */
21
+ run: (context: TaskContext & TranslationContext) => unknown;
22
+ }
23
+ /** A task that runs every so often. */
24
+ export interface EveryTaskConfig extends TaskConfigBase {
25
+ /**
26
+ * How often the task runs: a number and a unit, `s`, `m`, `h` or `d`,
27
+ * like `'30s'`, `'10m'`, `'2h'`, `'1d'` or `'1h30m'`. Once per second at
28
+ * most. Counted from when the bot connects.
29
+ */
30
+ every: string;
31
+ cron?: never;
32
+ timezone?: never;
33
+ }
34
+ /** A task that runs at given times. */
35
+ export interface CronTaskConfig extends TaskConfigBase {
36
+ /**
37
+ * When the task runs, as a cron expression: minute, hour, day of month,
38
+ * month and day of week, like `'0 9 * * 1-5'` (9:00 on weekdays). `*`,
39
+ * lists (`1,15`), ranges (`1-5`), steps (`*​/15`) and names (`mon`,
40
+ * `jan`) work.
41
+ */
42
+ cron: string;
43
+ /**
44
+ * The timezone the cron follows, like `'Europe/Paris'`. The clock of
45
+ * the machine by default.
46
+ */
47
+ timezone?: string;
48
+ every?: never;
49
+ }
50
+ /** What a task file gives to `task()`: when it runs, and what it does. */
51
+ export type TaskConfig = EveryTaskConfig | CronTaskConfig;
52
+ /** What `task()` returns: the default export of a task file. */
53
+ export interface TaskFile {
54
+ /** What the file gave to `task()`, not checked yet. */
55
+ readonly config: unknown;
56
+ }
57
+ /**
58
+ * Declares a task: something the bot does by itself. Export the result as
59
+ * the default export of a file of `src/tasks/`: the name of the file is
60
+ * the name of the task.
61
+ *
62
+ * ```ts
63
+ * import { task } from 'chapterjs';
64
+ *
65
+ * export default task({
66
+ * every: '10m',
67
+ * async run({ guilds }) {
68
+ * console.log(`Watching ${guilds.size} servers`);
69
+ * },
70
+ * });
71
+ * ```
72
+ */
73
+ export declare function task(config: TaskConfig): TaskFile;
74
+ /** Whether a value was made by `task()`. */
75
+ export declare function isTaskFile(value: unknown): value is TaskFile;
76
+ export {};
@@ -0,0 +1,28 @@
1
+ // `task()`, as the files of `src/tasks/` import it from 'chapterjs': what
2
+ // the bot does by itself, at intervals or at given times.
3
+ const BRAND = Symbol.for('chapterjs.task');
4
+ /**
5
+ * Declares a task: something the bot does by itself. Export the result as
6
+ * the default export of a file of `src/tasks/`: the name of the file is
7
+ * the name of the task.
8
+ *
9
+ * ```ts
10
+ * import { task } from 'chapterjs';
11
+ *
12
+ * export default task({
13
+ * every: '10m',
14
+ * async run({ guilds }) {
15
+ * console.log(`Watching ${guilds.size} servers`);
16
+ * },
17
+ * });
18
+ * ```
19
+ */
20
+ export function task(config) {
21
+ return Object.freeze({ [BRAND]: true, config });
22
+ }
23
+ /** Whether a value was made by `task()`. */
24
+ export function isTaskFile(value) {
25
+ return (typeof value === 'object' &&
26
+ value !== null &&
27
+ value[BRAND] === true);
28
+ }
@@ -0,0 +1,13 @@
1
+ type CamelKey<S extends string> = S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<CamelKey<Tail>>}` : S;
2
+ /** A Discord payload type with every field name turned into camelCase. */
3
+ export type Camelize<T> = T extends readonly (infer Item)[] ? Camelize<Item>[] : T extends object ? {
4
+ [K in keyof T as K extends string ? CamelKey<K> : K]: Camelize<T[K]>;
5
+ } : T;
6
+ /**
7
+ * What users write → what Discord expects. Dates become ISO8601 text,
8
+ * `undefined` fields are dropped.
9
+ */
10
+ export declare function toSnakeCase<T>(value: Camelize<T>): T;
11
+ /** What Discord sends → what users read. */
12
+ export declare function toCamelCase<T>(value: T): Camelize<T>;
13
+ export {};
@@ -0,0 +1,54 @@
1
+ // Discord names fields in snake_case, JavaScript in camelCase. Users only
2
+ // ever write and read camelCase: these two conversions are the single place
3
+ // where names are translated, for every feature.
4
+ // Only real field names are converted: ids (`"123"`) and locales (`"en-US"`),
5
+ // used as keys of some objects, must stay as they are.
6
+ const CAMEL = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)+$/;
7
+ const SNAKE = /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/;
8
+ function isPlainObject(value) {
9
+ if (typeof value !== 'object' || value === null)
10
+ return false;
11
+ const proto = Object.getPrototypeOf(value);
12
+ return proto === Object.prototype || proto === null;
13
+ }
14
+ function convert(value, rename) {
15
+ if (Array.isArray(value))
16
+ return value.map(item => convert(item, rename));
17
+ if (!isPlainObject(value)) {
18
+ // Something that knows how to become JSON (a set of permissions...).
19
+ if (typeof value === 'object' &&
20
+ value !== null &&
21
+ 'toJSON' in value &&
22
+ typeof value.toJSON === 'function' &&
23
+ !(value instanceof Date)) {
24
+ return value.toJSON();
25
+ }
26
+ if (value instanceof Date)
27
+ return value.toISOString();
28
+ return value;
29
+ }
30
+ const result = {};
31
+ for (const [key, item] of Object.entries(value)) {
32
+ if (item === undefined)
33
+ continue;
34
+ result[rename(key)] = convert(item, rename);
35
+ }
36
+ return result;
37
+ }
38
+ const toSnakeKey = (key) => CAMEL.test(key)
39
+ ? key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`)
40
+ : key;
41
+ const toCamelKey = (key) => SNAKE.test(key)
42
+ ? key.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase())
43
+ : key;
44
+ /**
45
+ * What users write → what Discord expects. Dates become ISO8601 text,
46
+ * `undefined` fields are dropped.
47
+ */
48
+ export function toSnakeCase(value) {
49
+ return convert(value, toSnakeKey);
50
+ }
51
+ /** What Discord sends → what users read. */
52
+ export function toCamelCase(value) {
53
+ return convert(value, toCamelKey);
54
+ }
@@ -0,0 +1,8 @@
1
+ /** How a duration is written, for a message that says what to write. */
2
+ export declare const DURATION_EXAMPLE = "a number and a unit: s, m, h or d, like '30s', '2h', '1d' or '1h30m'";
3
+ /**
4
+ * The milliseconds of a duration: a number and a unit, `s`, `m`, `h` or
5
+ * `d`. Several parts add up (`'1h30m'`), spaces and case do not matter.
6
+ * `null` when the text is not a duration.
7
+ */
8
+ export declare function parseDuration(text: string): number | null;
@@ -0,0 +1,29 @@
1
+ // A duration written by a user: `'30s'`, `'10m'`, `'1h30m'`. Read once here
2
+ // for every feature that takes one (the `every` of a task, the `cooldown`
3
+ // of a command); each feature gives its own message when it is not one.
4
+ /** How a duration is written, for a message that says what to write. */
5
+ export const DURATION_EXAMPLE = "a number and a unit: s, m, h or d, like '30s', '2h', '1d' or '1h30m'";
6
+ const UNITS = {
7
+ s: 1000,
8
+ m: 60_000,
9
+ h: 3_600_000,
10
+ d: 86_400_000,
11
+ };
12
+ /**
13
+ * The milliseconds of a duration: a number and a unit, `s`, `m`, `h` or
14
+ * `d`. Several parts add up (`'1h30m'`), spaces and case do not matter.
15
+ * `null` when the text is not a duration.
16
+ */
17
+ export function parseDuration(text) {
18
+ const compact = text.replace(/\s+/g, '');
19
+ const parts = [...compact.matchAll(/(\d+(?:\.\d+)?)([smhd])/gi)];
20
+ const whole = parts.map(part => part[0]).join('');
21
+ if (parts.length === 0 || whole.toLowerCase() !== compact.toLowerCase()) {
22
+ return null;
23
+ }
24
+ let ms = 0;
25
+ for (const [, amount, unit] of parts) {
26
+ ms += Number(amount) * UNITS[unit.toLowerCase()];
27
+ }
28
+ return Math.round(ms);
29
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Calls `onEnd` once when a WebSocket ends, with its close code.
3
+ *
4
+ * A socket that could not connect fires `error`, then `close` with 1006.
5
+ * Node 22 fired only `error` and left the socket connecting forever: a
6
+ * `close` that has not come by the next turn of the event loop is taken as
7
+ * that 1006, so how a socket ends never depends on the version of Node.
8
+ */
9
+ export declare function onSocketEnd(ws: WebSocket, onEnd: (code: number) => void): void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Calls `onEnd` once when a WebSocket ends, with its close code.
3
+ *
4
+ * A socket that could not connect fires `error`, then `close` with 1006.
5
+ * Node 22 fired only `error` and left the socket connecting forever: a
6
+ * `close` that has not come by the next turn of the event loop is taken as
7
+ * that 1006, so how a socket ends never depends on the version of Node.
8
+ */
9
+ export function onSocketEnd(ws, onEnd) {
10
+ let ended = false;
11
+ const end = (code) => {
12
+ if (ended)
13
+ return;
14
+ ended = true;
15
+ onEnd(code);
16
+ };
17
+ ws.addEventListener('close', event => end(event.code));
18
+ ws.addEventListener('error', () => {
19
+ setImmediate(() => {
20
+ if (ended)
21
+ return;
22
+ end(1006);
23
+ try {
24
+ ws.close();
25
+ }
26
+ catch {
27
+ // Already closed or never opened: nothing to release.
28
+ }
29
+ });
30
+ });
31
+ }