libp2r2p 0.11.22 → 0.11.24

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
@@ -264,7 +264,36 @@ close. Missing-key retries have no polling timer. MAC failures permit three tota
264
264
  attempts, persisted across reloads; exhausted packets remain until FIFO eviction.
265
265
  `retryPendingDecryptions({ force: true })` explicitly retries exhausted packets.
266
266
  Success enters the normal acknowledged inbox before removing the pending item.
267
- The session coordinator forwards this API. Private-channel consumers may opt in
267
+ The session coordinator forwards this API.
268
+
269
+ Pending recovery waits for 250 ms without channel setup, live/history processing
270
+ or publication activity. Session foreground persistence/publication also delays
271
+ it. After at most five seconds of eligible waiting, a busy channel may advance
272
+ one packet, then receives a fresh deadline. Idle channels take batches of up to
273
+ four records, yielding a task between every record. A busy channel does not hold
274
+ up other channels; overdue channels precede the preferred foreground channel.
275
+ Only requested passes run: a missing key never starts a polling loop. Timer
276
+ throttling or an in-flight signer/storage call can extend actual elapsed time.
277
+
278
+ `pendingDecryptionReady` defaults to `true` on messenger/session construction.
279
+ Apps may pass `false` and call `setPendingDecryptionReady(true)` after their
280
+ initial interface is presented; `false` holds only pending recovery, not live
281
+ reception, publication or channel setup. The idle/deadline windows start after
282
+ release. `setPendingDecryptionPriority(pubkey | null)` selects a channel on the
283
+ messenger or a peer on the session; self-chat/`null` clear the session preference.
284
+ The session retains a peer preference until preparation finishes and clears it
285
+ on peer removal. Keep startup release and account lifecycle fencing in the app.
286
+
287
+ `retryPendingDecryptions()` resolves after the requested scheduled pass (including
288
+ an aggregated follow-up). `force: true` only overrides the MAC attempt ceiling;
289
+ it does not bypass availability or admission. Requests before the first snapshot
290
+ share that pass; requests during it aggregate into one following pass. Queue
291
+ reads wait for initial release. Each packet is revalidated under the identity
292
+ Web Lock when available; waits hold neither that lock nor the normal inbox/IDB
293
+ queues. Fragment assembly survives scheduling boundaries. Pause/close cancel
294
+ waits; channel removal fences its queued and in-flight results.
295
+
296
+ Private-channel consumers may opt in
268
297
  with `onDecryptionFailure({ error, record })`, returning true only after durable
269
298
  handling, and replay records with `replayDeferredDecryptions`. Without the hook,
270
299
  standalone readers retain their existing error behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.22",
3
+ "version": "0.11.24",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -717,7 +717,7 @@ function createProcessor ({
717
717
  onNymEvent,
718
718
  onSeedEvent,
719
719
  onContentKeyUsage,
720
- onDecryptionFailure, isDecryptionPending,
720
+ onDecryptionFailure, isDecryptionPending, _onProcessing,
721
721
  onError,
722
722
  receivedChunkScope = '',
723
723
  receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS,
@@ -741,7 +741,14 @@ function createProcessor ({
741
741
  storeReady.catch(() => {})
742
742
 
743
743
  async function processOuterEvent (outer) {
744
- await storeReady
744
+ const release = _onProcessing?.(outer.pubkey)
745
+ try {
746
+ await storeReady
747
+ return await processReadyOuterEvent(outer)
748
+ } finally { release?.() }
749
+ }
750
+
751
+ async function processReadyOuterEvent (outer) {
745
752
  let groupKey = ''
746
753
  let routerDecrypted = false
747
754
  try {
@@ -982,7 +989,7 @@ function shouldIgnoreGroupError (err) {
982
989
 
983
990
  // Replay ciphertext only. Keep one fragment processor for the entire batch;
984
991
  // incomplete outer groups remain pending until a later key/channel transition.
985
- export async function replayDeferredDecryptions ({ records, onRecovered, onFailure, ...options }) {
992
+ export async function replayDeferredDecryptions ({ records, onRecovered, onFailure, _runRecord = (_record, work) => work(), ...options }) {
986
993
  if (!Array.isArray(records) || records.some(record => typeof record?.id !== 'string' ||
987
994
  typeof record.channelPubkey !== 'string' || !['outer_v1', 'routerRow_v1'].includes(record.packet?.format))) throw new ValidationError('INVALID_DEFERRED_RECORDS')
988
995
  if (typeof onRecovered !== 'function' || typeof onFailure !== 'function') throw new ValidationError('DEFERRED_CALLBACKS_REQUIRED')
@@ -1017,44 +1024,48 @@ export async function replayDeferredDecryptions ({ records, onRecovered, onFailu
1017
1024
  })
1018
1025
  try {
1019
1026
  for (const record of records) {
1020
- options.signal?.throwIfAborted()
1021
- current = record
1022
- chunked = false
1023
- try {
1024
- if (record.packet.format === 'outer_v1') {
1025
- await processor(record.packet.outer)
1026
- if (!chunked) await onRecovered(record)
1027
- } else {
1028
- const event = await unwrapRouterEvent({
1029
- receiverSigner: options.receiverSigner, receiverPubkey: options.receiverPubkey,
1030
- channelPubkey: record.channelPubkey, router: record.packet.router
1031
- })
1032
- options.signal?.throwIfAborted()
1033
- if (event) {
1034
- await options.onEvent?.(event, record.packet.outer, {
1035
- router: record.packet.router, channelPubkey: record.channelPubkey,
1036
- ...deliveryInfo(event, readSenderTag(record.packet.router))
1027
+ await _runRecord(record, async () => {
1028
+ options.signal?.throwIfAborted()
1029
+ current = record
1030
+ chunked = false
1031
+ try {
1032
+ if (record.packet.format === 'outer_v1') {
1033
+ await processor(record.packet.outer)
1034
+ if (!chunked) await onRecovered(record)
1035
+ } else {
1036
+ const event = await unwrapRouterEvent({
1037
+ receiverSigner: options.receiverSigner, receiverPubkey: options.receiverPubkey,
1038
+ channelPubkey: record.channelPubkey, router: record.packet.router
1037
1039
  })
1040
+ options.signal?.throwIfAborted()
1041
+ if (event) {
1042
+ await options.onEvent?.(event, record.packet.outer, {
1043
+ router: record.packet.router, channelPubkey: record.channelPubkey,
1044
+ ...deliveryInfo(event, readSenderTag(record.packet.router))
1045
+ })
1046
+ }
1047
+ await onRecovered(record)
1038
1048
  }
1039
- await onRecovered(record)
1049
+ } catch (error) {
1050
+ options.signal?.throwIfAborted()
1051
+ await onFailure(record, decryptionError(error, { channelPubkey: record.channelPubkey, eventId: record.packet.outer?.id }))
1040
1052
  }
1041
- } catch (error) {
1042
- options.signal?.throwIfAborted()
1043
- await onFailure(record, decryptionError(error, { channelPubkey: record.channelPubkey, eventId: record.packet.outer?.id }))
1044
- }
1053
+ for (const key of completed) {
1054
+ for (const id of groups.get(key) || []) {
1055
+ const recovered = records.find(record => record.id === id)
1056
+ if (recovered) await onRecovered(recovered)
1057
+ }
1058
+ groups.delete(key)
1059
+ }
1060
+ completed.clear()
1061
+ })
1045
1062
  // Let live ingestion and foreground signer work run between records.
1046
1063
  await new Promise(resolve => setTimeout(resolve, 0))
1047
1064
  }
1048
- for (const key of completed) {
1049
- for (const id of groups.get(key) || []) {
1050
- const record = records.find(record => record.id === id)
1051
- if (record) await onRecovered(record)
1052
- }
1053
- }
1054
1065
  } finally { processor.close() }
1055
1066
  }
1056
1067
 
1057
- export async function fetch ({ signal, receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, onError, since, until, limit, mode = 'leecher', modeByPubkey, receivedChunkScope = '', receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _getEvents = getEvents }) {
1068
+ export async function fetch ({ signal, receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, _onProcessing, onError, since, until, limit, mode = 'leecher', modeByPubkey, receivedChunkScope = '', receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _getEvents = getEvents }) {
1058
1069
  if (!relays?.length) throw new ValidationError('NO_RELAYS')
1059
1070
  const authors = privateChannelPubkeyList({ privateChannelPubkey, privateChannelPubkeys })
1060
1071
  const filter = { kinds: [PRIVATE_BROADCAST_KIND] }
@@ -1073,7 +1084,7 @@ export async function fetch ({ signal, receiverSigner, iykcSigner, privateChanne
1073
1084
  const elapsedMs = Math.round(performance.now() - startedAt)
1074
1085
  const events = result.map(({ event }) => event)
1075
1086
  events.sort((a, b) => a.created_at - b.created_at)
1076
- const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, onError, receivedChunkScope, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
1087
+ const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, _onProcessing, onError, receivedChunkScope, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
1077
1088
  try {
1078
1089
  for (const event of events) {
1079
1090
  signal?.throwIfAborted()
@@ -1103,7 +1114,7 @@ export async function fetchHistory ({ receiverSigner, privateChannelSigner = rec
1103
1114
  } finally { processOuterEvent.close() }
1104
1115
  }
1105
1116
 
1106
- export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, onError, since = nowSeconds() - 5, limit, liveOnly = false, mode = 'leecher', modeByPubkey, receivedChunkScope = '', receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _liveEventsGenerator = getLiveEventsGenerator, _eventsFeedGenerator = getEventsFeedGenerator }) {
1117
+ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, _onProcessing, onError, since = nowSeconds() - 5, limit, liveOnly = false, mode = 'leecher', modeByPubkey, receivedChunkScope = '', receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _liveEventsGenerator = getLiveEventsGenerator, _eventsFeedGenerator = getEventsFeedGenerator }) {
1107
1118
  if (!relays?.length) throw new ValidationError('NO_RELAYS')
1108
1119
  if (receiverSigner && !receiverSigner?.nip44DecryptDoubleDH && !receiverSigner?.nip44v3Decrypt) throw new ValidationError('RECEIVER_SIGNER_NIP44V3_DECRYPT_UNSUPPORTED')
1109
1120
  if (!privateChannelReaderSigner && !privateChannelReaderSignersByPubkey && !privateChannelSigner && !privateChannelSignersByPubkey) throw new ValidationError('PRIVATE_CHANNEL_READER_REQUIRED')
@@ -1112,7 +1123,7 @@ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner =
1112
1123
  const filter = { kinds: [PRIVATE_BROADCAST_KIND], since }
1113
1124
  if (authors.length) filter.authors = authors
1114
1125
  if (limit != null) filter.limit = limit
1115
- const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, onError, receivedChunkScope, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
1126
+ const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, _onProcessing, onError, receivedChunkScope, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
1116
1127
  const controller = new AbortController()
1117
1128
  const events = liveOnly
1118
1129
  ? _liveEventsGenerator(filter, relays, {
@@ -477,6 +477,7 @@ export function createPrivateMessageSession ({ _setTimeout = setTimeout, _clearT
477
477
  return dispatchSeedEvent(seed)
478
478
  },
479
479
  onContentKeyUsage: dispatchContentKeyUsage,
480
+ _onProcessing: pubkey => watchCallbacks(pubkey)._onProcessing?.(pubkey),
480
481
  isDecryptionPending: record => watchCallbacks(record.channelPubkey).isDecryptionPending?.(record),
481
482
  onDecryptionFailure: failure => watchCallbacks(failure.record.channelPubkey).onDecryptionFailure?.(failure),
482
483
  onError: err => reportSubscriptionError(channels, err)
@@ -524,7 +525,7 @@ export function createPrivateMessageSession ({ _setTimeout = setTimeout, _clearT
524
525
  onSeed,
525
526
  onChunk,
526
527
  onContentKeyUsage,
527
- onDecryptionFailure, isDecryptionPending,
528
+ onDecryptionFailure, isDecryptionPending, _onProcessing,
528
529
  onSubscriptionState,
529
530
  onError,
530
531
  receivedChunkTtlMs,
@@ -545,7 +546,7 @@ export function createPrivateMessageSession ({ _setTimeout = setTimeout, _clearT
545
546
  const ownPubkey = receiverPubkey || await receiverSigner?.getPublicKey?.()
546
547
  if (identity !== undefined && identity !== ownPubkey) throw new ValidationError('PRIVATE_MESSAGE_IDENTITY_MISMATCH')
547
548
  identity = ownPubkey
548
- const callbacks = { onAsk, onReply, onTell, onYell, onNym, onMessage, onSeed, onChunk, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, onSubscriptionState, onError }
549
+ const callbacks = { onAsk, onReply, onTell, onYell, onNym, onMessage, onSeed, onChunk, onContentKeyUsage, onDecryptionFailure, isDecryptionPending, _onProcessing, onSubscriptionState, onError }
549
550
 
550
551
  for (const channel of channelList) {
551
552
  if (watchRequests.get(channel) !== request) continue