libp2r2p 0.10.26 → 0.10.27

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
@@ -519,6 +519,21 @@ This also covers abrupt termination without `close()` and channels with no
519
519
  messages. Applications still supply signers and desired channels on reopening;
520
520
  storage is scoped by origin and user pubkey, not the transient session ID.
521
521
  Failed ingestion or incomplete relay fetch reports never complete the gap.
522
+ `private-channel.fetch` reports incomplete reads as an `AggregateError` with
523
+ stable `code: 'PRIVATE_CHANNEL_FETCH_INCOMPLETE'`. Its message names relays,
524
+ completion statuses and native failure messages, including status-only failures.
525
+ `relays` retains all `{ relay, status, error? }` outcomes; `relayErrors` preserves
526
+ `{ relay, reason }` attribution and `errors` retains unique native causes from
527
+ both sources. `request` records selected relays, channel pubkeys, receiver pubkey,
528
+ `since`/`until`/`limit` and the 5000ms read timeout. `elapsedMs` measures the read
529
+ including admission wait, excluding decryption/storage; `receivedEventCount`
530
+ counts fetched outer events, not delivered messages. No event bodies or signers
531
+ are attached. These diagnostics reach the messenger's `onError` unchanged.
532
+ A single incomplete relay keeps the interval pending even if another relay
533
+ returned events; received messages may already have been queued. This is a
534
+ history completeness failure, not evidence that an outgoing message failed.
535
+ Existing rewatch/resume and relay-list refresh paths retry pending intervals;
536
+ there is no dedicated periodic retry timer for a failed history fetch.
522
537
  `lastSeenAt` tracks messages persisted in the incoming queue, not app commits.
523
538
  The default recovery window is seven days; recovery still depends on available
524
539
  relay/seeder data, and retention limits cannot guarantee indefinite delivery.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -0,0 +1,26 @@
1
+ const describe = reason => `${reason?.category ? `[${reason.category}] ` : ''}${reason?.message || String(reason)}`
2
+
3
+ export function incompleteFetchError ({ errors, report, request, receivedEventCount, elapsedMs }) {
4
+ // Keep only read metadata, never events, decrypted payloads or signers.
5
+ const relayErrors = errors.map(({ relay, reason }) => ({ relay, reason }))
6
+ const relays = report.map(({ relay, status, error }) => ({ relay, status, ...(error ? { error } : {}) }))
7
+ const reasons = [...new Set([...relayErrors.map(entry => entry.reason), ...relays.map(entry => entry.error)].filter(reason => reason !== undefined))]
8
+ const details = relays.map(({ relay, status, error }) => {
9
+ const failures = relayErrors.filter(entry => entry.relay === relay).map(entry => entry.reason)
10
+ if (error && !failures.includes(error)) failures.push(error)
11
+ return `${relay} [${status}]${failures.length ? `: ${failures.map(describe).join('; ')}` : ''}`
12
+ })
13
+ for (const { relay, reason } of relayErrors) {
14
+ if (!relays.some(entry => entry.relay === relay)) details.push(`${relay || 'unknown relay'}: ${describe(reason)}`)
15
+ }
16
+ return Object.assign(new AggregateError(reasons,
17
+ `PRIVATE_CHANNEL_FETCH_INCOMPLETE: ${details.join(' | ')} (received=${receivedEventCount}, readElapsedMs=${elapsedMs}, timeoutMs=${request.timeoutMs})`), {
18
+ code: 'PRIVATE_CHANNEL_FETCH_INCOMPLETE',
19
+ operation: 'private-channel.fetch',
20
+ request,
21
+ receivedEventCount,
22
+ elapsedMs,
23
+ relays,
24
+ relayErrors
25
+ })
26
+ }
@@ -6,6 +6,7 @@ import { isValidContentKeyProof, isValidIykcProof, makeContentKeyEventForPubkey,
6
6
  import { getIykcProofs } from '../content-key/index.js'
7
7
  import { ValidationError } from '../error/index.js'
8
8
  import { normalizeRumor, deliveryInfo } from './helpers/rumor.js'
9
+ import { incompleteFetchError } from './helpers/fetch-error.js'
9
10
  import * as nip44v3 from '../nip44-v3/index.js'
10
11
  import { relayPool } from '../relay/index.js'
11
12
  import { JSONL_CHUNK_BYTES, NYM_CARRIER_CHUNK_CHARS } from './helpers/chunk-size.js'
@@ -907,11 +908,14 @@ export async function fetch ({ signal, receiverSigner, iykcSigner, privateChanne
907
908
  if (until != null) filter.until = until
908
909
  if (limit != null) filter.limit = limit
909
910
 
911
+ const timeoutMs = 5000
912
+ const startedAt = performance.now()
910
913
  const { result, errors = [], relays: report = [] } = await _getEvents(filter, relays, {
911
- timeout: 5000,
914
+ timeout: timeoutMs,
912
915
  timeoutAfterFirstEose: null,
913
916
  ...(signal ? { signal } : {})
914
917
  })
918
+ const elapsedMs = Math.round(performance.now() - startedAt)
915
919
  const events = result.map(({ event }) => event)
916
920
  events.sort((a, b) => a.created_at - b.created_at)
917
921
  const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkScope, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
@@ -922,7 +926,10 @@ export async function fetch ({ signal, receiverSigner, iykcSigner, privateChanne
922
926
  }
923
927
  signal?.throwIfAborted()
924
928
  if (errors.length || report.some(entry => !['eose', 'satisfied'].includes(entry.status))) {
925
- throw new AggregateError(errors.map(entry => entry.reason), 'PRIVATE_CHANNEL_FETCH_INCOMPLETE')
929
+ throw incompleteFetchError({
930
+ errors, report, elapsedMs, receivedEventCount: events.length,
931
+ request: { relays: [...relays], channelPubkeys: [...authors], receiverPubkey, since, until, limit, timeoutMs }
932
+ })
926
933
  }
927
934
  return events
928
935
  } finally {