@evanp/activitypub-bot 0.45.18 → 0.45.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,24 @@ and this project adheres to
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.45.20] - 2026-05-14
13
+
14
+ ### Fixed
15
+
16
+ - Handle some pathological formats for request throttle resets -- epoch in
17
+ ms and offset in ms.
18
+
19
+ ## [0.45.19] - 2026-05-14
20
+
21
+ ### Changed
22
+
23
+ - Use connection keep-alive for outgoing requests in SafeAgent.
24
+
25
+ ### Fixed
26
+
27
+ - Use AbortSignal.timeout() in fetch() in ActivityPubClient instead of obsolete
28
+ and ignored `timeout` option.
29
+
12
30
  ## [0.45.18] - 2026-05-12
13
31
 
14
32
  ### Fixed
@@ -538,7 +538,7 @@ export class ActivityPubClient {
538
538
  const fullOptions = {
539
539
  ...options,
540
540
  agent: this.#agent ?? undefined,
541
- timeout: 10000,
541
+ signal: options?.signal ?? AbortSignal.timeout(10000),
542
542
  size: 1024 * 1024,
543
543
  follow: 10
544
544
  }
@@ -2,7 +2,8 @@ import { setTimeout as sleep } from 'node:timers/promises'
2
2
  import assert from 'node:assert'
3
3
 
4
4
  const BETA = 0.75
5
- const EPOCH_THRESHOLD = 30 * 24 * 60 * 60
5
+ const OFFSET_THRESHOLD = 30 * 24 * 60 * 60
6
+ const DEFAULT_RESET = 30 * 1000
6
7
 
7
8
  export class ThrottleError extends Error {
8
9
  constructor (message, waitTime) {
@@ -43,25 +44,33 @@ export class RequestThrottler {
43
44
  assert.strictEqual(typeof host, 'string')
44
45
  assert.strictEqual(typeof headers, 'object')
45
46
 
47
+ const retryAfterHeader = headers.get('retry-after')
46
48
  const resetHeader = headers.get('x-ratelimit-reset')
47
49
  const remainingHeader = headers.get('x-ratelimit-remaining')
48
50
 
49
- if (resetHeader && remainingHeader) {
51
+ if (retryAfterHeader) {
52
+ const remaining = 0
53
+ const reset = this.#headerToReset(retryAfterHeader)
54
+ this.#logger.debug(
55
+ { reset, remaining, host, retryAfterHeader },
56
+ 'updating'
57
+ )
58
+ await this.#connection.query(
59
+ `INSERT INTO rate_limit (host, remaining, reset)
60
+ VALUES (?, ?, ?)
61
+ ON CONFLICT (host) DO UPDATE
62
+ SET remaining = EXCLUDED.remaining,
63
+ reset = EXCLUDED.reset,
64
+ updated_at = CURRENT_TIMESTAMP`,
65
+ { replacements: [host, remaining, reset] }
66
+ )
67
+ } else if (resetHeader && remainingHeader) {
50
68
  const remaining = parseInt(remainingHeader)
51
- let resetSeconds
52
- let reset
53
- if (resetHeader.match(/^\d+$/)) {
54
- resetSeconds = parseInt(resetHeader)
55
- if (resetSeconds < EPOCH_THRESHOLD) {
56
- reset = new Date(Date.now() + (resetSeconds * 1000))
57
- } else {
58
- reset = new Date(resetSeconds * 1000)
59
- }
60
- } else {
61
- reset = new Date(resetHeader)
62
- resetSeconds = reset - Date.now()
63
- }
64
- this.#logger.debug({ reset, remaining, host }, 'updating')
69
+ const reset = this.#headerToReset(resetHeader)
70
+ this.#logger.debug(
71
+ { reset, remaining, host, resetHeader, remainingHeader },
72
+ 'updating'
73
+ )
65
74
  await this.#connection.query(
66
75
  `INSERT INTO rate_limit (host, remaining, reset)
67
76
  VALUES (?, ?, ?)
@@ -165,4 +174,39 @@ export class RequestThrottler {
165
174
  { replacements: [host] }
166
175
  )
167
176
  }
177
+
178
+ #headerToReset (header) {
179
+ let reset
180
+ const now = Date.now()
181
+ if (header.match(/^\d+$/)) {
182
+ const num = parseInt(header)
183
+
184
+ if (num > now - (3600 * 1000)) {
185
+ // epoch in ms
186
+ reset = new Date(num)
187
+ } else if (num > (now / 1000) - 3600) {
188
+ // epoch in s
189
+ reset = new Date(num * 1000)
190
+ } else if (num < OFFSET_THRESHOLD) {
191
+ // offset in s
192
+ reset = new Date(now + (num * 1000))
193
+ } else if (num < OFFSET_THRESHOLD * 1000) {
194
+ // offset in ms
195
+ reset = new Date(now + num)
196
+ } else {
197
+ // no good guesses; default
198
+ reset = new Date(now + DEFAULT_RESET)
199
+ }
200
+ } else {
201
+ reset = new Date(header)
202
+ if (Number.isNaN(reset.getTime())) {
203
+ this.#logger.warn(
204
+ { header },
205
+ 'Error parsing header for request throttling'
206
+ )
207
+ reset = new Date(now + DEFAULT_RESET)
208
+ }
209
+ }
210
+ return reset
211
+ }
168
212
  }
package/lib/safeagent.js CHANGED
@@ -19,6 +19,16 @@ class PrivateNetworkError extends Error {
19
19
  }
20
20
 
21
21
  export class SafeAgent extends https.Agent {
22
+ constructor (options = {}) {
23
+ super({
24
+ keepAlive: true,
25
+ keepAliveMsecs: 1000,
26
+ maxSockets: 64,
27
+ maxFreeSockets: 256,
28
+ ...options
29
+ })
30
+ }
31
+
22
32
  createConnection (options, callback) {
23
33
  dns.lookup(options.hostname, (err, address) => {
24
34
  if (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evanp/activitypub-bot",
3
- "version": "0.45.18",
3
+ "version": "0.45.20",
4
4
  "description": "server-side ActivityPub bot framework",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",