libp2r2p 0.12.0 → 0.12.2

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.
@@ -8,6 +8,7 @@ import { maybeUnref } from '../helpers/timer.js'
8
8
  import { categorizeRelayError, relayTimeoutError } from '../helpers/error.js'
9
9
  import { ReadRetry, isRetryableReadFailure, needsConnectivityCheck, waitUntil } from '../helpers/read-retry.js'
10
10
  import { normalizeRelayUrl } from '../../url/index.js'
11
+ import { isValidEvent } from '../../event/index.js'
11
12
  import { RelayConnection } from './relay-connection.js'
12
13
 
13
14
  const CONNECTION_TIMEOUT_MS = 3000
@@ -69,7 +70,19 @@ function notifyRelayResult (onRelayResult, result) {
69
70
  }
70
71
 
71
72
  function requiresNip42Auth (reason) {
72
- return reason.message.startsWith('auth-required:') || reason.message.startsWith('restricted:')
73
+ const message = reason?.message || reason?.code || ''
74
+ return message.startsWith('auth-required:') || message.startsWith('restricted:')
75
+ }
76
+
77
+ function requiresOuterRefresh (reason) {
78
+ return requiresNip42Auth(reason) || (reason?.message || reason?.code || '').startsWith('invalid:')
79
+ }
80
+
81
+ function isDefinitiveOuterRejection (report) {
82
+ return report?.success === false &&
83
+ Array.isArray(report.errors) &&
84
+ report.errors.length > 0 &&
85
+ report.errors.every(({ reason }) => requiresOuterRefresh(reason))
73
86
  }
74
87
 
75
88
  function countResponseError () {
@@ -1199,15 +1212,19 @@ export class RelayPool {
1199
1212
  // { relay, success, outcome, reason? } result per relay as it settles; outcome
1200
1213
  // is published, duplicate, muted, failed, or timed-out. getAuthEvent is used
1201
1214
  // only after auth-required or restricted publish failures, then retries once.
1215
+ // getRetryEvent may return one fresh signed event after every relay rejected
1216
+ // the original with auth-required/restricted/invalid; it shares the operation deadline.
1202
1217
  // Await `promise` for the complete report, including every relay outcome.
1203
1218
  async sendEvent (event, relays, {
1204
1219
  timeout = SEND_TIMEOUT_MS,
1205
1220
  timeoutUntilFirstFulfillment = null,
1206
1221
  getAuthEvent,
1222
+ getRetryEvent,
1207
1223
  onRelayResult,
1208
1224
  signal
1209
1225
  } = {}) {
1210
1226
  if (signal != null && (typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function' || typeof signal.removeEventListener !== 'function')) throw new ValidationError('INVALID_RELAY_SIGNAL')
1227
+ if (getRetryEvent != null && typeof getRetryEvent !== 'function') throw new ValidationError('INVALID_RETRY_EVENT_CALLBACK')
1211
1228
  const urls = normalizedRelayUrls(relays)
1212
1229
  if (!urls.length) {
1213
1230
  const promise = Promise.resolve(publishSummary([], urls, {
@@ -1216,69 +1233,121 @@ export class RelayPool {
1216
1233
  return { total: 0, success: false, promise }
1217
1234
  }
1218
1235
 
1219
- const eventToSend = event.meta ? { ...event } : event
1220
- if (eventToSend.meta) delete eventToSend.meta
1221
-
1222
- const sendControllers = urls.map(() => new AbortController())
1223
- const sendDeferreds = urls.map(() => Promise.withResolvers())
1224
- const sendPromises = sendDeferreds.map(({ promise }) => promise)
1225
-
1226
- // Starts before connection work so every relay shares one real deadline.
1227
- const settlement = createPublishSettlements(sendPromises, timeout, {
1228
- onSettled: (settlement, index) => {
1229
- sendControllers[index].abort(settlement.reason)
1230
- if (settlement.reason?.category === 'timeout' && !settlement.reason.cause) {
1231
- const relay = this.#relays.get(normalizeRelayUrl(urls[index]))
1232
- if (relay?.lastTransportError) settlement.reason.cause = relay.lastTransportError
1236
+ const deadline = timeout === null ? null : Date.now() + timeout
1237
+ const remainingTimeout = () => deadline === null ? null : Math.max(0, deadline - Date.now())
1238
+ const sendAttempt = async (eventToSend, { notify = true } = {}) => {
1239
+ const attemptTimeout = remainingTimeout()
1240
+ const eventForAttempt = eventToSend.meta ? { ...eventToSend } : eventToSend
1241
+ if (eventForAttempt.meta) delete eventForAttempt.meta
1242
+ const sendControllers = urls.map(() => new AbortController())
1243
+ const sendDeferreds = urls.map(() => Promise.withResolvers())
1244
+ const sendPromises = sendDeferreds.map(({ promise }) => promise)
1245
+ const bufferedResults = []
1246
+ const delivery = { notify }
1247
+
1248
+ // Starts before connection work so every relay shares one real deadline.
1249
+ const settlement = createPublishSettlements(sendPromises, attemptTimeout, {
1250
+ onSettled: (settlement, index) => {
1251
+ sendControllers[index].abort(settlement.reason)
1252
+ if (settlement.reason?.category === 'timeout' && !settlement.reason.cause) {
1253
+ const relay = this.#relays.get(normalizeRelayUrl(urls[index]))
1254
+ if (relay?.lastTransportError) settlement.reason.cause = relay.lastTransportError
1255
+ }
1256
+ const result = relayResultForSettlement(urls[index], settlement)
1257
+ if (delivery.notify) notifyRelayResult(onRelayResult, result)
1258
+ else bufferedResults.push(result)
1233
1259
  }
1234
- notifyRelayResult(onRelayResult, relayResultForSettlement(urls[index], settlement))
1235
- }
1236
- })
1237
-
1238
- // Resolves after every relay settles (or reaches the operation timeout) as
1239
- // { success, total, fulfilled, succeededRelays, errors },
1240
- // where errors contains { relay, reason } entries for failed relays.
1241
- const cancel = () => settlement.cancel(signal.reason)
1242
- if (signal?.aborted) cancel()
1243
- else signal?.addEventListener('abort', cancel, { once: true })
1244
- const promise = settlement.promise
1245
- .finally(() => signal?.removeEventListener('abort', cancel))
1246
- .then(settlements => publishSummary(settlements, urls, {
1247
- includeSucceededRelays: true
1248
- }))
1260
+ })
1249
1261
 
1250
- urls.forEach((url, index) => {
1251
- const deferred = sendDeferreds[index]
1252
- ;(async () => {
1253
- try {
1254
- const operationSignal = sendControllers[index].signal
1255
- if (operationSignal.aborted) throw operationSignal.reason
1256
- const relay = await this.#getRelay(url)
1257
- if (operationSignal.aborted) throw operationSignal.reason
1258
- return await this.#publishEvent(relay, eventToSend, getAuthEvent, operationSignal)
1259
- } catch (err) {
1260
- const reason = err instanceof Error ? err : new Error(String(err))
1261
- if (reason instanceof Nip42AuthenticationError) throw reason
1262
- if (reason.message.startsWith('duplicate:')) return 'duplicate'
1263
- if (reason.message.startsWith('mute:')) {
1264
- console.info([url, reason.message].filter(Boolean).join(' - '))
1265
- return 'muted'
1262
+ // Resolves after every relay settles (or reaches the operation timeout) as
1263
+ // { success, total, fulfilled, succeededRelays, errors },
1264
+ // where errors contains { relay, reason } entries for failed relays.
1265
+ const cancel = () => settlement.cancel(signal.reason)
1266
+ if (signal?.aborted) cancel()
1267
+ else signal?.addEventListener('abort', cancel, { once: true })
1268
+ const promise = settlement.promise
1269
+ .finally(() => signal?.removeEventListener('abort', cancel))
1270
+ .then(settlements => publishSummary(settlements, urls, {
1271
+ includeSucceededRelays: true
1272
+ }))
1273
+
1274
+ urls.forEach((url, index) => {
1275
+ const deferred = sendDeferreds[index]
1276
+ ;(async () => {
1277
+ try {
1278
+ const operationSignal = sendControllers[index].signal
1279
+ if (operationSignal.aborted) throw operationSignal.reason
1280
+ const relay = await this.#getRelay(url)
1281
+ if (operationSignal.aborted) throw operationSignal.reason
1282
+ return await this.#publishEvent(relay, eventForAttempt, getAuthEvent, operationSignal)
1283
+ } catch (err) {
1284
+ const reason = err instanceof Error ? err : new Error(String(err))
1285
+ if (reason instanceof Nip42AuthenticationError) throw reason
1286
+ if (reason.message.startsWith('duplicate:')) return 'duplicate'
1287
+ if (reason.message.startsWith('mute:')) {
1288
+ console.info([url, reason.message].filter(Boolean).join(' - '))
1289
+ return 'muted'
1290
+ }
1291
+ throw reason
1266
1292
  }
1267
- throw reason
1268
- }
1269
- })().then(deferred.resolve, deferred.reject)
1270
- })
1293
+ })().then(deferred.resolve, deferred.reject)
1294
+ })
1271
1295
 
1272
- const success = await firstFulfillment(sendPromises, timeoutUntilFirstFulfillment, {
1273
- fallback: promise.then(report => report.success)
1274
- })
1275
- if (!success) settlement.timeout()
1296
+ const earlyTimeout = timeoutUntilFirstFulfillment === null || attemptTimeout === null
1297
+ ? timeoutUntilFirstFulfillment
1298
+ : Math.min(timeoutUntilFirstFulfillment, attemptTimeout)
1299
+ const success = await firstFulfillment(sendPromises, earlyTimeout, {
1300
+ fallback: promise.then(report => report.success)
1301
+ })
1302
+ if (!success) settlement.timeout()
1276
1303
 
1277
- return {
1278
- total: urls.length,
1279
- success,
1280
- promise
1304
+ const result = {
1305
+ total: urls.length,
1306
+ success,
1307
+ promise
1308
+ }
1309
+ Object.defineProperty(result, 'flushRelayResults', {
1310
+ value: () => {
1311
+ delivery.notify = true
1312
+ for (const result of bufferedResults.splice(0)) notifyRelayResult(onRelayResult, result)
1313
+ }
1314
+ })
1315
+ return result
1316
+ }
1317
+
1318
+ const first = await sendAttempt(event, { notify: !getRetryEvent })
1319
+ if (first.success || !getRetryEvent) {
1320
+ first.flushRelayResults()
1321
+ return first
1322
+ }
1323
+ let report
1324
+ try {
1325
+ report = await first.promise
1326
+ } catch {
1327
+ first.flushRelayResults()
1328
+ return first
1329
+ }
1330
+ if (!isDefinitiveOuterRejection(report) || (deadline !== null && Date.now() >= deadline)) {
1331
+ first.flushRelayResults()
1332
+ return first
1333
+ }
1334
+ let retryEvent
1335
+ try {
1336
+ retryEvent = await getRetryEvent({ event, report })
1337
+ } catch (cause) {
1338
+ throw Object.assign(new Error('RETRY_EVENT_PREPARATION_FAILED', { cause }), {
1339
+ code: 'RETRY_EVENT_PREPARATION_FAILED',
1340
+ cause
1341
+ })
1342
+ }
1343
+ if ((deadline !== null && Date.now() >= deadline) ||
1344
+ !isValidEvent(retryEvent) || retryEvent.id === event.id || retryEvent.kind !== event.kind) {
1345
+ first.flushRelayResults()
1346
+ return first
1281
1347
  }
1348
+ const retried = await sendAttempt(retryEvent, { notify: true })
1349
+ Object.defineProperty(retried, 'retriedEvent', { value: retryEvent })
1350
+ return retried
1282
1351
  }
1283
1352
  }
1284
1353