libp2r2p 0.11.19 → 0.11.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/README.md CHANGED
@@ -955,22 +955,26 @@ also admits relay policy refusals (`blocked:`, `restricted:`, `auth-required:`,
955
955
  Local authentication denials, invalid events and unknown errors qualify for
956
956
  neither policy. Predicates classify individual native errors; callers decide
957
957
  how to handle mixed per-relay outcomes and application-specific aggregates.
958
- `parseRelayRetryAdvice(reason, extra, { now = Date.now() })`, exported by
958
+ `parseRelayRetryAdvice(reason, params, { now = Date.now() })`, exported by
959
959
  `libp2r2p/relay`, returns `{ retryAt, retryAfterMs? }` or `null`. It only interprets
960
- leading `rate-limited:` refusals. Optional `retry_at` is a Unix timestamp in
961
- seconds (fractions allowed), converted to milliseconds. Finite positive absolute
962
- advice takes precedence over `retry_after`, including when already expired;
963
- expired advice does not restart a relative wait. Future waits are capped at five
964
- minutes from processing. Missing/invalid absolute advice falls back to finite
965
- positive relative seconds, capped at 300. A valid relative value is retained as
966
- `retryAfterMs` even when the absolute deadline wins. Schedulers wait until
960
+ leading `rate-limited:` refusals. The optional string `params` is relative
961
+ milliseconds (e.g. "2500") or an absolute Unix timestamp in milliseconds
962
+ prefixed with `@` (e.g. "@1791738154332"). Values must be positive safe integers
963
+ in canonical decimal form, without signs, whitespace, fractions or leading zeros.
964
+ Relative values produce both `retryAfterMs` and `retryAt`; absolute values produce
965
+ only `retryAt` and retain an expired deadline without starting a new wait.
966
+ Future waits are capped at five minutes. Missing or invalid params are ignored
967
+ and do not instruct clients to retry immediately. Schedulers wait until
967
968
  max(local backoff deadline, retryAt), never sum the two or renew the deadline on
968
969
  online notifications.
969
970
 
970
- The optional Nostr extension works with older relays that only send
971
- `retry_after` and older consumers that ignore `retry_at`. Neither field supplies
972
- provenance or retry eligibility: `local`, `origin`, `retryable`, category and code
973
- claims in extras are ignored. Native messages, causes and codes remain intact.
971
+ Rate limits apply only to the client message type that triggered the refusal:
972
+ `EVENT`, `AUTH`, `REQ` and `COUNT` have independent connection cooldowns. Responses
973
+ are correlated with pending operations; unknown IDs never create cooldowns.
974
+ CLOSE cleanup remains immediate. The launcher can replace valid relative params
975
+ with `@` timestamps before queueing bridge delivery because its consumers share
976
+ its clock. No legacy timing objects are interpreted. Params never supply provenance
977
+ or retry eligibility; native messages, causes and codes remain intact.
974
978
 
975
979
  ## Binary encodings
976
980
 
@@ -1091,10 +1095,11 @@ user-facing send error.
1091
1095
 
1092
1096
  ### Relay backpressure and bounded synchronization (0.11.7)
1093
1097
 
1094
- Optional `retry_at` Unix seconds or numeric `retry_after` seconds on relay `CLOSED` and rejected `OK`
1095
- responses are preserved as `retryAfterMs` and `retryAt` on native relay errors.
1096
- Only `rate-limited:` responses apply this advice; invalid values are ignored
1097
- and delays are capped at five minutes. New operations on that connection wait
1098
+ Optional decimal `params` milliseconds or `@` Unix milliseconds on relay `CLOSED`
1099
+ and rejected `OK` responses are preserved as normalized `retryAt` (and
1100
+ `retryAfterMs` for relative params) on native relay errors. Only `rate-limited:`
1101
+ responses apply this advice; invalid values are ignored
1102
+ and delays are capped at five minutes. New operations of the refused type wait
1098
1103
  within their existing deadlines; active subscriptions remain live and `CLOSE`
1099
1104
  never waits. Cancelled or timed-out operations cannot be sent later. A rejection
1100
1105
  is still reported to the caller; this does not silently republish failed events.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.19",
3
+ "version": "0.11.20",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -33,8 +33,8 @@ export function relayCloseError (event, category, cause) {
33
33
 
34
34
  // Keep native diagnostics and attach only bounded timing advice. An absolute
35
35
  // deadline survives delayed delivery through a launcher-owned relay bridge.
36
- export function relayRejectionError (reason, extra, fallback) {
36
+ export function relayRejectionError (reason, params, fallback) {
37
37
  const error = categorizeRelayError(reason, 'relay', fallback)
38
- Object.assign(error, parseRelayRetryAdvice(error.message, extra))
38
+ Object.assign(error, parseRelayRetryAdvice(error.message, params))
39
39
  return error
40
40
  }
@@ -1,18 +1,13 @@
1
- // Nostr transport extensions, in seconds. Timing advice is bounded and never
1
+ // Nostr params use relative milliseconds or @Unix milliseconds. Advice never
2
2
  // supplies an error category, provenance or a routing/retry policy.
3
- export function parseRelayRetryAdvice (reason, extra, { now = Date.now() } = {}) {
3
+ export function parseRelayRetryAdvice (reason, params, { now = Date.now() } = {}) {
4
4
  if (typeof reason !== 'string' || !reason.startsWith('rate-limited:') ||
5
- !extra || typeof extra !== 'object' || Array.isArray(extra) || !Number.isFinite(now)) return null
5
+ typeof params !== 'string' || params.match(/^@?[1-9]\d*$/)?.[0] !== params || !Number.isFinite(now)) return null
6
6
 
7
- const after = extra.retry_after
8
- const retryAfterMs = typeof after === 'number' && Number.isFinite(after) && after > 0
9
- ? Math.min(after, 300) * 1000
10
- : undefined
11
- const at = extra.retry_at
12
- const absolute = typeof at === 'number' && Number.isFinite(at) && at > 0 && Number.isFinite(at * 1000)
13
- ? Math.min(at * 1000, now + 300000)
14
- : undefined
15
- const retryAt = absolute ?? (retryAfterMs === undefined ? undefined : now + retryAfterMs)
16
- if (retryAt === undefined) return null
17
- return { ...(retryAfterMs === undefined ? {} : { retryAfterMs }), retryAt }
7
+ const absolute = params.startsWith('@')
8
+ const value = Number(absolute ? params.slice(1) : params)
9
+ if (!Number.isSafeInteger(value)) return null
10
+ if (absolute) return { retryAt: Math.min(value, now + 300000) }
11
+ const retryAfterMs = Math.min(value, 300000)
12
+ return { retryAfterMs, retryAt: now + retryAfterMs }
18
13
  }
@@ -50,7 +50,7 @@ export class RelayConnection {
50
50
  #publishes = new Map()
51
51
  #authentications = new Map()
52
52
  #counts = new Map()
53
- #retryAt = 0
53
+ #retryAtByType = new Map()
54
54
 
55
55
  constructor (url, { WebSocket: WebSocketImpl = globalThis.WebSocket } = {}) {
56
56
  this.url = url
@@ -124,12 +124,12 @@ export class RelayConnection {
124
124
 
125
125
  // New work respects relay cooldowns; cleanup uses send() directly. These
126
126
  // waits stay within the caller's existing deadline and are cancellable.
127
- #dispatchWork (send, fail) {
127
+ #dispatchWork (type, send, fail) {
128
128
  let timer = null
129
129
  let cancelled = false
130
130
  const dispatch = () => {
131
131
  if (cancelled) return
132
- const remaining = this.#retryAt - Date.now()
132
+ const remaining = (this.#retryAtByType.get(type) ?? 0) - Date.now()
133
133
  if (remaining > 0) {
134
134
  timer = maybeUnref(setTimeout(dispatch, remaining))
135
135
  return
@@ -140,9 +140,9 @@ export class RelayConnection {
140
140
  return () => { cancelled = true; clearTimeout(timer) }
141
141
  }
142
142
 
143
- #rejection (reason, extra, fallback) {
144
- const error = relayRejectionError(reason, extra, fallback)
145
- this.#retryAt = Math.max(this.#retryAt, error.retryAt || 0)
143
+ #rejection (type, reason, params, fallback) {
144
+ const error = relayRejectionError(reason, params, fallback)
145
+ if (error.retryAt !== undefined) this.#retryAtByType.set(type, Math.max(this.#retryAtByType.get(type) ?? 0, error.retryAt))
146
146
  return error
147
147
  }
148
148
 
@@ -163,7 +163,7 @@ export class RelayConnection {
163
163
  }
164
164
  const subscription = { filters, handlers, close, cancelSend: null }
165
165
  this.#subscriptions.set(id, subscription)
166
- subscription.cancelSend = this.#dispatchWork(() => {
166
+ subscription.cancelSend = this.#dispatchWork('REQ', () => {
167
167
  this.send(JSON.stringify(['REQ', id, ...filters]))
168
168
  sent = true
169
169
  }, error => {
@@ -209,7 +209,7 @@ export class RelayConnection {
209
209
  pending.waiters.add(waiter)
210
210
  signal?.addEventListener('abort', onAbort, { once: true })
211
211
  if (created) {
212
- pending.cancelSend = this.#dispatchWork(() => {
212
+ pending.cancelSend = this.#dispatchWork(type, () => {
213
213
  this.send(JSON.stringify([type, event]))
214
214
  }, error => this.#settleEvent(map, event.id, error))
215
215
  }
@@ -224,7 +224,7 @@ export class RelayConnection {
224
224
  const pending = { ...deferred, signal, onAbort, cancelSend: null }
225
225
  this.#counts.set(id, pending)
226
226
  signal?.addEventListener('abort', onAbort, { once: true })
227
- pending.cancelSend = this.#dispatchWork(() => this.send(JSON.stringify(['COUNT', id, ...filters])), error => {
227
+ pending.cancelSend = this.#dispatchWork('COUNT', () => this.send(JSON.stringify(['COUNT', id, ...filters])), error => {
228
228
  this.#settleCount(id, null, error)
229
229
  })
230
230
  return deferred.promise
@@ -277,7 +277,8 @@ export class RelayConnection {
277
277
  if (data[0] === 'CLOSED') {
278
278
  const id = data[1]
279
279
  const subscription = this.#subscriptions.get(id)
280
- const reason = this.#rejection(data[2], data[3], subscription ? 'SUBSCRIPTION_CLOSED' : 'COUNT_CLOSED')
280
+ if (!subscription && !this.#counts.has(id)) return
281
+ const reason = this.#rejection(subscription ? 'REQ' : 'COUNT', data[2], data[3], subscription ? 'SUBSCRIPTION_CLOSED' : 'COUNT_CLOSED')
281
282
  if (subscription) {
282
283
  this.#subscriptions.delete(id)
283
284
  subscription.cancelSend?.()
@@ -286,9 +287,11 @@ export class RelayConnection {
286
287
  return
287
288
  }
288
289
  if (data[0] === 'OK') {
289
- const reason = data[2] === true ? null : this.#rejection(data[3], data[4], 'EVENT_REJECTED')
290
- this.#settleEvent(this.#publishes, data[1], reason, data[3])
291
- this.#settleEvent(this.#authentications, data[1], reason, data[3])
290
+ for (const [type, pending] of [['EVENT', this.#publishes], ['AUTH', this.#authentications]]) {
291
+ if (!pending.has(data[1])) continue
292
+ const reason = data[2] === true ? null : this.#rejection(type, data[3], data[4], 'EVENT_REJECTED')
293
+ this.#settleEvent(pending, data[1], reason, data[3])
294
+ }
292
295
  return
293
296
  }
294
297
  if (data[0] === 'AUTH' && typeof data[1] === 'string') {