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 +30 -1
- package/package.json +1 -1
- package/private-channel/index.js +46 -35
- package/private-message/index.js +3 -2
- package/private-messenger/index.js +257 -177
- package/private-messenger/services/decryption-scheduler.js +135 -0
- package/private-messenger/services/pending-decryption.js +6 -0
- package/private-messenger/session/README.md +37 -1
- package/private-messenger/session/helpers/foreground-work.js +6 -1
- package/private-messenger/session/index.js +128 -28
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.
|
|
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
package/private-channel/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
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
|
-
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
options.signal?.throwIfAborted()
|
|
1051
|
+
await onFailure(record, decryptionError(error, { channelPubkey: record.channelPubkey, eventId: record.packet.outer?.id }))
|
|
1040
1052
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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, {
|
package/private-message/index.js
CHANGED
|
@@ -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
|