libp2r2p 0.11.22 → 0.11.23
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 +13 -0
- package/private-messenger/session/helpers/foreground-work.js +6 -1
- package/private-messenger/session/index.js +21 -2
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
|
|
@@ -56,6 +56,7 @@ import { normalizeRelayUrl } from '../url/index.js'
|
|
|
56
56
|
import { deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
57
57
|
import { migrateMessageQueue } from './services/migrate-message-queue.js'
|
|
58
58
|
import { createPendingDecryption } from './services/pending-decryption.js'
|
|
59
|
+
import { createDecryptionScheduler } from './services/decryption-scheduler.js'
|
|
59
60
|
import { decryptionError, isDeferredDecryptionError } from '../private-channel/helpers/decryption-error.js'
|
|
60
61
|
import { bytesToBase64 } from '../base64/index.js'
|
|
61
62
|
import { ValidationError } from '../error/index.js'
|
|
@@ -236,6 +237,7 @@ export class PrivateMessenger {
|
|
|
236
237
|
autoDeletionCapability = true,
|
|
237
238
|
_indexedDB = globalThis.indexedDB,
|
|
238
239
|
useContentKeys = true,
|
|
240
|
+
pendingDecryptionReady = true,
|
|
239
241
|
onContentKeyChange,
|
|
240
242
|
onMessageQueued,
|
|
241
243
|
onStateChanged,
|
|
@@ -248,6 +250,8 @@ export class PrivateMessenger {
|
|
|
248
250
|
_subscribeRelayListUpdates = subscribeRelayListUpdates,
|
|
249
251
|
_isOnline = isOnline,
|
|
250
252
|
_waitForForeground,
|
|
253
|
+
_observeForeground,
|
|
254
|
+
_now = Date.now,
|
|
251
255
|
_onOnline = onOnline,
|
|
252
256
|
_random = Math.random,
|
|
253
257
|
_setTimeout = globalThis.setTimeout.bind(globalThis),
|
|
@@ -259,6 +263,7 @@ export class PrivateMessenger {
|
|
|
259
263
|
_BroadcastChannel = _indexedDB === globalThis.indexedDB ? globalThis.BroadcastChannel : undefined
|
|
260
264
|
} = {}) {
|
|
261
265
|
if (onStateChanged != null && typeof onStateChanged !== 'function') throw new ValidationError('INVALID_ON_STATE_CHANGED')
|
|
266
|
+
if (typeof pendingDecryptionReady !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
262
267
|
this.fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
263
268
|
this.fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
|
|
264
269
|
this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
|
|
@@ -383,6 +388,9 @@ export class PrivateMessenger {
|
|
|
383
388
|
this.closePromise = null
|
|
384
389
|
this.initSettledPromise = null
|
|
385
390
|
this.initialized = false
|
|
391
|
+
this.pendingDecryptionReady = pendingDecryptionReady
|
|
392
|
+
this.decryptionScheduler = createDecryptionScheduler({ ready: pendingDecryptionReady, now: _now, setTimer: _setTimeout, clearTimer: _clearTimeout })
|
|
393
|
+
this.stopDecryptionForeground = _observeForeground?.(active => this.decryptionScheduler.foreground(active))
|
|
386
394
|
this.notifyStatus()
|
|
387
395
|
}
|
|
388
396
|
|
|
@@ -1222,6 +1230,7 @@ export class PrivateMessenger {
|
|
|
1222
1230
|
onSeed: seed => this.receive(pubkey, () => this.enqueueSeed(pubkey, seed), seed),
|
|
1223
1231
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
1224
1232
|
onDecryptionFailure: failure => this.deferDecryption(failure),
|
|
1233
|
+
_onProcessing: pubkey => this.decryptionScheduler.activity(pubkey),
|
|
1225
1234
|
isDecryptionPending: record => this.pendingDecryption.has(record),
|
|
1226
1235
|
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1227
1236
|
receivedChunkIndexedDB: this._indexedDB,
|
|
@@ -1734,7 +1743,10 @@ export class PrivateMessenger {
|
|
|
1734
1743
|
unwatch (channels) {
|
|
1735
1744
|
const pubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.desiredChannels]
|
|
1736
1745
|
for (const extension of this.extensions) extension.unwatch?.(pubkeys)
|
|
1737
|
-
for (const pubkey of pubkeys)
|
|
1746
|
+
for (const pubkey of pubkeys) {
|
|
1747
|
+
this.desiredChannels.delete(pubkey)
|
|
1748
|
+
this.decryptionScheduler.remove(pubkey)
|
|
1749
|
+
}
|
|
1738
1750
|
for (const [key, pending] of this.pendingStorageWrites) if (pubkeys.includes(pending.record.channelPubkey)) this.pendingStorageWrites.delete(key)
|
|
1739
1751
|
this.syncPauseRecovery()
|
|
1740
1752
|
for (const pubkey of pubkeys) this.dropRecoveryState(pubkey)
|
|
@@ -1904,6 +1916,7 @@ export class PrivateMessenger {
|
|
|
1904
1916
|
if (job) { jobs.push(job.work); continue }
|
|
1905
1917
|
const controller = new AbortController()
|
|
1906
1918
|
const channel = this.channels.get(pubkey)
|
|
1919
|
+
const releaseActivity = this.decryptionScheduler.activity(pubkey)
|
|
1907
1920
|
job = { controller, work: null }
|
|
1908
1921
|
this.channelBackground.set(pubkey, job)
|
|
1909
1922
|
const active = () => this.initialized && !controller.signal.aborted && !this.closePromise && !this.pauseReasons.size && this.channels.get(pubkey) === channel && this.desiredChannels.has(pubkey)
|
|
@@ -1940,6 +1953,7 @@ export class PrivateMessenger {
|
|
|
1940
1953
|
throw error
|
|
1941
1954
|
})
|
|
1942
1955
|
job.work = Promise.race([work, cancelled]).finally(() => {
|
|
1956
|
+
releaseActivity()
|
|
1943
1957
|
controller.signal.removeEventListener('abort', stopAbort)
|
|
1944
1958
|
if (this.channelBackground.get(pubkey) === job) this.channelBackground.delete(pubkey)
|
|
1945
1959
|
})
|
|
@@ -1949,18 +1963,19 @@ export class PrivateMessenger {
|
|
|
1949
1963
|
return jobs
|
|
1950
1964
|
}
|
|
1951
1965
|
|
|
1952
|
-
receive (pubkey, operation, message = {}) {
|
|
1966
|
+
receive (pubkey, operation, message = {}, { trackActivity = true } = {}) {
|
|
1953
1967
|
if (this.closePromise) {
|
|
1954
1968
|
if (this.storageActive) this.recordInterruption([pubkey], messageTime(message))
|
|
1955
1969
|
return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
1956
1970
|
}
|
|
1971
|
+
const releaseActivity = trackActivity ? this.decryptionScheduler.activity(pubkey) : () => {}
|
|
1957
1972
|
return this.queueIncoming(async () => {
|
|
1958
1973
|
if (this.pauseReasons.size || !this.desiredChannels.has(pubkey)) {
|
|
1959
1974
|
this.recordInterruption([pubkey], messageTime(message))
|
|
1960
1975
|
throw this.pausedError()
|
|
1961
1976
|
}
|
|
1962
1977
|
return operation()
|
|
1963
|
-
})
|
|
1978
|
+
}).finally(releaseActivity)
|
|
1964
1979
|
}
|
|
1965
1980
|
|
|
1966
1981
|
nip65WatchChannelPubkeys () {
|
|
@@ -2274,180 +2289,204 @@ export class PrivateMessenger {
|
|
|
2274
2289
|
|
|
2275
2290
|
async ask ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
2276
2291
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2292
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2293
|
+
try {
|
|
2294
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [receiverPubkey], relays, relayToReceivers })
|
|
2295
|
+
this.debugSend('ask', channelPubkey, { code, receiverPubkey })
|
|
2296
|
+
return await this._privateMessage.ask({
|
|
2297
|
+
senderSigner: this.userSigner,
|
|
2298
|
+
imkcSigner: this.contentKeySigner,
|
|
2299
|
+
privateChannelSigner: channel.signer,
|
|
2300
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2301
|
+
receiverPubkey,
|
|
2302
|
+
...routing,
|
|
2303
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2304
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2305
|
+
deletionPubkey,
|
|
2306
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2307
|
+
message,
|
|
2308
|
+
code,
|
|
2309
|
+
payload,
|
|
2310
|
+
error,
|
|
2311
|
+
content,
|
|
2312
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2313
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2314
|
+
})
|
|
2315
|
+
} finally { releaseActivity() }
|
|
2298
2316
|
}
|
|
2299
2317
|
|
|
2300
2318
|
async reply ({ channelPubkey = this.defaultChannelPubkey(), question, receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
2301
2319
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2302
|
-
const
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2320
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2321
|
+
try {
|
|
2322
|
+
const resolvedReceiverPubkey = receiverPubkey || question?.pubkey || ''
|
|
2323
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [resolvedReceiverPubkey], relays, relayToReceivers })
|
|
2324
|
+
this.debugSend('reply', channelPubkey, { code, receiverPubkey: receiverPubkey || question?.pubkey || '' })
|
|
2325
|
+
return await this._privateMessage.reply({
|
|
2326
|
+
senderSigner: this.userSigner,
|
|
2327
|
+
imkcSigner: this.contentKeySigner,
|
|
2328
|
+
privateChannelSigner: channel.signer,
|
|
2329
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2330
|
+
question,
|
|
2331
|
+
receiverPubkey,
|
|
2332
|
+
...routing,
|
|
2333
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2334
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2335
|
+
deletionPubkey,
|
|
2336
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2337
|
+
message,
|
|
2338
|
+
code,
|
|
2339
|
+
payload,
|
|
2340
|
+
error,
|
|
2341
|
+
content,
|
|
2342
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2343
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2344
|
+
})
|
|
2345
|
+
} finally { releaseActivity() }
|
|
2325
2346
|
}
|
|
2326
2347
|
|
|
2327
2348
|
async tell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
2328
2349
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2351
|
+
try {
|
|
2352
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [receiverPubkey], relays, relayToReceivers })
|
|
2353
|
+
this.debugSend('tell', channelPubkey, { code, receiverPubkey })
|
|
2354
|
+
return await this._privateMessage.tell({
|
|
2355
|
+
senderSigner: this.userSigner,
|
|
2356
|
+
imkcSigner: this.contentKeySigner,
|
|
2357
|
+
privateChannelSigner: channel.signer,
|
|
2358
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2359
|
+
receiverPubkey,
|
|
2360
|
+
...routing,
|
|
2361
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2362
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2363
|
+
deletionPubkey,
|
|
2364
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2365
|
+
message,
|
|
2366
|
+
code,
|
|
2367
|
+
payload,
|
|
2368
|
+
error,
|
|
2369
|
+
content,
|
|
2370
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2371
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2372
|
+
})
|
|
2373
|
+
} finally { releaseActivity() }
|
|
2350
2374
|
}
|
|
2351
2375
|
|
|
2352
2376
|
async yell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
2353
2377
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2354
|
-
const
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2378
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2379
|
+
try {
|
|
2380
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
|
|
2381
|
+
this.debugSend('yell', channelPubkey, { code, receiverPubkeys })
|
|
2382
|
+
return await this._privateMessage.yell({
|
|
2383
|
+
senderSigner: this.userSigner,
|
|
2384
|
+
imkcSigner: this.contentKeySigner,
|
|
2385
|
+
privateChannelSigner: channel.signer,
|
|
2386
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2387
|
+
receiverPubkeys,
|
|
2388
|
+
...routing,
|
|
2389
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2390
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2391
|
+
deletionPubkey,
|
|
2392
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2393
|
+
message,
|
|
2394
|
+
code,
|
|
2395
|
+
payload,
|
|
2396
|
+
error,
|
|
2397
|
+
content,
|
|
2398
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2399
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2400
|
+
})
|
|
2401
|
+
} finally { releaseActivity() }
|
|
2375
2402
|
}
|
|
2376
2403
|
|
|
2377
2404
|
async broadcastRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, deletionPubkey, signal }) {
|
|
2378
2405
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2406
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2407
|
+
try {
|
|
2408
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers, signal })
|
|
2409
|
+
this.debugSend('broadcastRumor', channelPubkey, { receiverPubkeys })
|
|
2410
|
+
return await this._privateMessage.broadcastRumor({
|
|
2411
|
+
senderSigner: this.userSigner,
|
|
2412
|
+
imkcSigner: this.contentKeySigner,
|
|
2413
|
+
privateChannelSigner: channel.signer,
|
|
2414
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2415
|
+
receiverPubkeys,
|
|
2416
|
+
...routing,
|
|
2417
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2418
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2419
|
+
deletionPubkey,
|
|
2420
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2421
|
+
rumor,
|
|
2422
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2423
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2424
|
+
})
|
|
2425
|
+
} finally { releaseActivity() }
|
|
2396
2426
|
}
|
|
2397
2427
|
|
|
2398
2428
|
async broadcastEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, deletionPubkey, signal }) {
|
|
2399
2429
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2400
|
-
const
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2430
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2431
|
+
try {
|
|
2432
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers, signal })
|
|
2433
|
+
this.debugSend('broadcastEvent', channelPubkey, { receiverPubkeys })
|
|
2434
|
+
return await this._privateMessage.broadcastEvent({
|
|
2435
|
+
senderSigner: this.userSigner,
|
|
2436
|
+
imkcSigner: this.contentKeySigner,
|
|
2437
|
+
privateChannelSigner: channel.signer,
|
|
2438
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2439
|
+
receiverPubkeys,
|
|
2440
|
+
...routing,
|
|
2441
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2442
|
+
temporaryStorageArea: this.temporaryStorageArea,
|
|
2443
|
+
deletionPubkey,
|
|
2444
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2445
|
+
event,
|
|
2446
|
+
onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
|
|
2447
|
+
_getIykcProofs: this.contentKeyLookup()
|
|
2448
|
+
})
|
|
2449
|
+
} finally { releaseActivity() }
|
|
2417
2450
|
}
|
|
2418
2451
|
|
|
2419
2452
|
async broadcastNymRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, nymSigner, deletionPubkey }) {
|
|
2420
2453
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2421
|
-
const
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2454
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2455
|
+
try {
|
|
2456
|
+
const resolvedNymSigner = this.requireNymSigner(channel, nymSigner)
|
|
2457
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
|
|
2458
|
+
this.debugSend('broadcastNymRumor', channelPubkey, { receiverPubkeys })
|
|
2459
|
+
return await this._privateMessage.broadcastNymRumor({
|
|
2460
|
+
nymSigner: resolvedNymSigner,
|
|
2461
|
+
privateChannelSigner: channel.signer,
|
|
2462
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2463
|
+
...routing,
|
|
2464
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2465
|
+
deletionPubkey,
|
|
2466
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2467
|
+
rumor
|
|
2468
|
+
})
|
|
2469
|
+
} finally { releaseActivity() }
|
|
2434
2470
|
}
|
|
2435
2471
|
|
|
2436
2472
|
async broadcastNymEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, nymSigner, deletionPubkey }) {
|
|
2437
2473
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2438
|
-
const
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2474
|
+
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2475
|
+
try {
|
|
2476
|
+
const resolvedNymSigner = this.requireNymSigner(channel, nymSigner)
|
|
2477
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
|
|
2478
|
+
this.debugSend('broadcastNymEvent', channelPubkey, { receiverPubkeys })
|
|
2479
|
+
return await this._privateMessage.broadcastNymEvent({
|
|
2480
|
+
nymSigner: resolvedNymSigner,
|
|
2481
|
+
privateChannelSigner: channel.signer,
|
|
2482
|
+
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2483
|
+
...routing,
|
|
2484
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2485
|
+
deletionPubkey,
|
|
2486
|
+
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2487
|
+
event
|
|
2488
|
+
})
|
|
2489
|
+
} finally { releaseActivity() }
|
|
2451
2490
|
}
|
|
2452
2491
|
|
|
2453
2492
|
async publishSeederPresence (channelPubkey = this.defaultChannelPubkey(), { signal } = {}) {
|
|
@@ -2881,6 +2920,7 @@ export class PrivateMessenger {
|
|
|
2881
2920
|
onSeedEvent: seed => this.receive(channelPubkey, () => this.enqueueSeed(channelPubkey, seed), seed),
|
|
2882
2921
|
onContentKeyUsage: usage => this.handleContentKeyUsage(channelPubkey, usage),
|
|
2883
2922
|
onDecryptionFailure: failure => this.deferDecryption(failure),
|
|
2923
|
+
_onProcessing: pubkey => this.decryptionScheduler.activity(pubkey),
|
|
2884
2924
|
isDecryptionPending: record => this.pendingDecryption.has(record),
|
|
2885
2925
|
onError: err => { throw err }
|
|
2886
2926
|
}
|
|
@@ -3146,22 +3186,36 @@ export class PrivateMessenger {
|
|
|
3146
3186
|
}
|
|
3147
3187
|
}
|
|
3148
3188
|
|
|
3189
|
+
setPendingDecryptionReady (ready) {
|
|
3190
|
+
if (typeof ready !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
3191
|
+
this.pendingDecryptionReady = ready
|
|
3192
|
+
this.decryptionScheduler.setReady(ready)
|
|
3193
|
+
}
|
|
3194
|
+
|
|
3195
|
+
setPendingDecryptionPriority (pubkey) {
|
|
3196
|
+
if (pubkey !== null && !/^[0-9a-f]{64}$/.test(pubkey || '')) throw new ValidationError('INVALID_PENDING_DECRYPTION_PRIORITY')
|
|
3197
|
+
this.decryptionScheduler.priority(pubkey)
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3149
3200
|
retryPendingDecryptions ({ force = false } = {}) {
|
|
3150
3201
|
if (typeof force !== 'boolean') throw new ValidationError('INVALID_FORCE')
|
|
3151
3202
|
if (this.closePromise || this.pauseReasons.size || !this.initialized) return Promise.resolve()
|
|
3152
3203
|
if (this.decryptionRetry && !this.decryptionRetry.controller.signal.aborted) {
|
|
3153
|
-
|
|
3204
|
+
// Before a pass reads its snapshot, new requests belong to that pass.
|
|
3205
|
+
if (this.decryptionRetry.started) this.decryptionRetry.again = true
|
|
3154
3206
|
this.decryptionRetry.force ||= force
|
|
3155
3207
|
return this.decryptionRetry.work
|
|
3156
3208
|
}
|
|
3157
|
-
const job = { controller: new AbortController(), again: false, force, work: null }
|
|
3209
|
+
const job = { controller: new AbortController(), again: false, force, started: false, work: null }
|
|
3158
3210
|
const previous = this.decryptionRetry?.work
|
|
3159
3211
|
this.decryptionRetry = job
|
|
3160
3212
|
const { signal } = job.controller
|
|
3161
3213
|
const run = async () => {
|
|
3162
3214
|
do {
|
|
3215
|
+
await this.decryptionScheduler.waitReady(signal)
|
|
3163
3216
|
signal.throwIfAborted()
|
|
3164
3217
|
job.again = false
|
|
3218
|
+
job.started = true
|
|
3165
3219
|
const force = job.force
|
|
3166
3220
|
job.force = false
|
|
3167
3221
|
const groups = new Map()
|
|
@@ -3169,35 +3223,59 @@ export class PrivateMessenger {
|
|
|
3169
3223
|
if (!groups.has(record.channelPubkey)) groups.set(record.channelPubkey, [])
|
|
3170
3224
|
groups.get(record.channelPubkey).push(record)
|
|
3171
3225
|
}
|
|
3172
|
-
|
|
3173
|
-
signal.throwIfAborted()
|
|
3226
|
+
const results = await Promise.allSettled([...groups].map(async ([channelPubkey, records]) => {
|
|
3174
3227
|
const channel = this.channels.get(channelPubkey)
|
|
3175
|
-
if (!channel || !this.desiredChannels.has(channelPubkey))
|
|
3176
|
-
const current = () => {
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3228
|
+
if (!channel || !this.desiredChannels.has(channelPubkey)) return
|
|
3229
|
+
const current = () => {
|
|
3230
|
+
signal.throwIfAborted()
|
|
3231
|
+
if (this.pauseReasons.size || this.channels.get(channelPubkey) !== channel || !this.desiredChannels.has(channelPubkey)) throw new Error('PRIVATE_CHANNEL_CHANGED')
|
|
3232
|
+
}
|
|
3233
|
+
try {
|
|
3234
|
+
await this._privateChannel.replayDeferredDecryptions({
|
|
3235
|
+
records, signal, receiverSigner: this.userSigner, receiverPubkey: this.userPubkey,
|
|
3236
|
+
privateChannelSigner: channel.signer, privateChannelReaderSigner: channel.readerSigner,
|
|
3237
|
+
privateChannelReaderPubkey: channel.readerPubkey, mode: channel.mode,
|
|
3238
|
+
receivedChunkScope: `${this.prefix}:pending`, receivedChunkIndexedDB: this._indexedDB,
|
|
3239
|
+
_runRecord: async (record, process) => {
|
|
3240
|
+
let done = false
|
|
3241
|
+
while (!done) {
|
|
3242
|
+
current()
|
|
3243
|
+
done = await this.decryptionScheduler.run(channelPubkey, () => {
|
|
3244
|
+
const attempt = async () => {
|
|
3245
|
+
current()
|
|
3246
|
+
if (!this.pendingDecryptionReady) return false
|
|
3247
|
+
if (await this.pendingDecryption.current(record, { force })) await process()
|
|
3248
|
+
return true
|
|
3249
|
+
}
|
|
3250
|
+
const locks = globalThis.navigator?.locks
|
|
3251
|
+
return locks?.request ? locks.request(`${this.prefix}:pending-decryption:retry`, { signal }, attempt) : attempt()
|
|
3252
|
+
}, signal)
|
|
3253
|
+
}
|
|
3254
|
+
},
|
|
3255
|
+
onEvent: (event, outer, meta) => {
|
|
3256
|
+
current()
|
|
3257
|
+
return this.receive(channelPubkey, () => this.enqueueRumor(eventType(event), channelPubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }, { trackActivity: false })
|
|
3258
|
+
},
|
|
3259
|
+
onNymEvent: (event, outer, meta) => {
|
|
3260
|
+
current()
|
|
3261
|
+
return this.receive(channelPubkey, () => this.enqueueRumor('nym', channelPubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }, { trackActivity: false })
|
|
3262
|
+
},
|
|
3263
|
+
onSeedEvent: seed => { current(); return this.receive(channelPubkey, () => this.enqueueSeed(channelPubkey, seed), seed, { trackActivity: false }) },
|
|
3264
|
+
onRecovered: record => { current(); return this.pendingDecryption.recovered(record) },
|
|
3265
|
+
onFailure: (record, error) => { current(); return this.pendingDecryption.failed(record, error) }
|
|
3266
|
+
})
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
if (this.channels.get(channelPubkey) === channel && this.desiredChannels.has(channelPubkey)) throw error
|
|
3269
|
+
} finally { this.decryptionScheduler.finish(channelPubkey) }
|
|
3270
|
+
}))
|
|
3271
|
+
const failed = results.find(result => result.status === 'rejected')
|
|
3272
|
+
if (failed) throw failed.reason
|
|
3273
|
+
job.started = false
|
|
3195
3274
|
} while (job.again)
|
|
3196
3275
|
}
|
|
3197
|
-
job.work = Promise.resolve(previous).catch(() => {}).then((
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
}).catch(error => { if (!signal.aborted) throw error }).finally(() => {
|
|
3276
|
+
job.work = Promise.resolve(previous).catch(() => {}).then(run).catch(error => {
|
|
3277
|
+
if (!signal.aborted) throw error
|
|
3278
|
+
}).finally(() => {
|
|
3201
3279
|
if (this.decryptionRetry === job) this.decryptionRetry = null
|
|
3202
3280
|
})
|
|
3203
3281
|
return job.work
|
|
@@ -3256,6 +3334,8 @@ export class PrivateMessenger {
|
|
|
3256
3334
|
|
|
3257
3335
|
close () {
|
|
3258
3336
|
this.decryptionRetry?.controller.abort(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
3337
|
+
this.decryptionScheduler.close()
|
|
3338
|
+
this.stopDecryptionForeground?.()
|
|
3259
3339
|
if (this.closePromise) return this.closePromise
|
|
3260
3340
|
const initSettledPromise = this.initSettledPromise
|
|
3261
3341
|
this.sendRoutingLifetime.abort()
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Admission only: never wait while holding an inbox operation, IDB transaction
|
|
2
|
+
// or the cross-instance retry lock. A busy channel cannot stall another one.
|
|
3
|
+
export function createDecryptionScheduler ({ ready = true, now = Date.now, setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
|
|
4
|
+
const channels = new Map()
|
|
5
|
+
const readyWaiters = new Set()
|
|
6
|
+
let timer
|
|
7
|
+
let running = false
|
|
8
|
+
let closed
|
|
9
|
+
let foreground = false
|
|
10
|
+
let foregroundAt = now()
|
|
11
|
+
let priority = null
|
|
12
|
+
let previous
|
|
13
|
+
let batch = 0
|
|
14
|
+
const stateFor = pubkey => {
|
|
15
|
+
if (!channels.has(pubkey)) channels.set(pubkey, { active: 0, at: now(), deadline: null, jobs: [] })
|
|
16
|
+
return channels.get(pubkey)
|
|
17
|
+
}
|
|
18
|
+
const quietAt = state => state.active || foreground ? Infinity : Math.max(state.at, foregroundAt) + 250
|
|
19
|
+
const schedule = () => {
|
|
20
|
+
if (timer !== undefined) clearTimer(timer)
|
|
21
|
+
timer = undefined
|
|
22
|
+
if (closed || !ready || running) return
|
|
23
|
+
const pending = [...channels.values()].filter(state => state.jobs.length)
|
|
24
|
+
if (!pending.length) return
|
|
25
|
+
const at = Math.min(...pending.map(state => Math.min(quietAt(state), state.deadline)))
|
|
26
|
+
timer = setTimer(pump, Math.max(0, at - now()))
|
|
27
|
+
}
|
|
28
|
+
async function pump () {
|
|
29
|
+
timer = undefined
|
|
30
|
+
if (closed || !ready || running) return
|
|
31
|
+
const time = now()
|
|
32
|
+
const eligible = [...channels].filter(([, state]) => state.jobs.length && Math.min(quietAt(state), state.deadline) <= time)
|
|
33
|
+
eligible.sort(([a, x], [b, y]) => {
|
|
34
|
+
const overdue = Number(y.deadline <= time) - Number(x.deadline <= time)
|
|
35
|
+
if (overdue) return overdue
|
|
36
|
+
if (x.deadline <= time && x.deadline !== y.deadline) return x.deadline - y.deadline
|
|
37
|
+
if (a === previous && batch >= 4) return 1
|
|
38
|
+
if (b === previous && batch >= 4) return -1
|
|
39
|
+
return Number(b === priority) - Number(a === priority)
|
|
40
|
+
})
|
|
41
|
+
if (!eligible.length) { schedule(); return }
|
|
42
|
+
const [pubkey, state] = eligible[0]
|
|
43
|
+
const job = state.jobs.shift()
|
|
44
|
+
running = true
|
|
45
|
+
if (previous === pubkey) batch++
|
|
46
|
+
else { previous = pubkey; batch = 1 }
|
|
47
|
+
try {
|
|
48
|
+
job.signal?.throwIfAborted()
|
|
49
|
+
job.resolve(await job.work())
|
|
50
|
+
} catch (error) { job.reject(error) } finally {
|
|
51
|
+
job.signal?.removeEventListener('abort', job.abort)
|
|
52
|
+
state.deadline = now() + 5000
|
|
53
|
+
running = false
|
|
54
|
+
// Always use a new task, including between records in the same batch.
|
|
55
|
+
schedule()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const rejectJobs = (state, error) => {
|
|
59
|
+
for (const job of state.jobs.splice(0)) {
|
|
60
|
+
job.signal?.removeEventListener('abort', job.abort)
|
|
61
|
+
job.reject(error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
activity (pubkey) {
|
|
66
|
+
if (closed) return () => {}
|
|
67
|
+
const state = stateFor(pubkey)
|
|
68
|
+
state.active++; state.at = now(); schedule()
|
|
69
|
+
let released = false
|
|
70
|
+
return () => {
|
|
71
|
+
if (released) return
|
|
72
|
+
released = true
|
|
73
|
+
state.active--; state.at = now(); schedule()
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
foreground (active) { foreground = active; foregroundAt = now(); schedule() },
|
|
77
|
+
priority (pubkey) { priority = pubkey; schedule() },
|
|
78
|
+
setReady (value) {
|
|
79
|
+
if (ready === value || closed) return
|
|
80
|
+
ready = value
|
|
81
|
+
if (ready) {
|
|
82
|
+
for (const state of channels.values()) { state.at = now(); state.deadline = state.jobs.length ? now() + 5000 : null }
|
|
83
|
+
for (const wake of [...readyWaiters]) wake()
|
|
84
|
+
}
|
|
85
|
+
schedule()
|
|
86
|
+
},
|
|
87
|
+
async waitReady (signal) {
|
|
88
|
+
while (true) {
|
|
89
|
+
signal?.throwIfAborted()
|
|
90
|
+
if (closed) throw closed
|
|
91
|
+
if (ready) return
|
|
92
|
+
await new Promise((resolve, reject) => {
|
|
93
|
+
const finish = () => {
|
|
94
|
+
readyWaiters.delete(finish); signal?.removeEventListener('abort', finish)
|
|
95
|
+
if (closed || signal?.aborted) reject(closed || signal.reason)
|
|
96
|
+
else resolve()
|
|
97
|
+
}
|
|
98
|
+
readyWaiters.add(finish); signal?.addEventListener('abort', finish, { once: true })
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
run (pubkey, work, signal) {
|
|
103
|
+
signal?.throwIfAborted()
|
|
104
|
+
if (closed) return Promise.reject(closed)
|
|
105
|
+
const state = stateFor(pubkey)
|
|
106
|
+
state.deadline ??= now() + 5000
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const job = { work, resolve, reject, signal }
|
|
109
|
+
job.abort = () => {
|
|
110
|
+
const index = state.jobs.indexOf(job)
|
|
111
|
+
if (index < 0) return
|
|
112
|
+
state.jobs.splice(index, 1); reject(signal.reason); schedule()
|
|
113
|
+
}
|
|
114
|
+
signal?.addEventListener('abort', job.abort, { once: true })
|
|
115
|
+
state.jobs.push(job); schedule()
|
|
116
|
+
})
|
|
117
|
+
},
|
|
118
|
+
remove (pubkey) {
|
|
119
|
+
const state = channels.get(pubkey)
|
|
120
|
+
if (state) rejectJobs(state, new Error('PRIVATE_CHANNEL_CHANGED'))
|
|
121
|
+
channels.delete(pubkey); schedule()
|
|
122
|
+
},
|
|
123
|
+
finish (pubkey) {
|
|
124
|
+
const state = channels.get(pubkey)
|
|
125
|
+
if (state && !state.jobs.length) state.deadline = null
|
|
126
|
+
},
|
|
127
|
+
close () {
|
|
128
|
+
closed ||= new Error('PRIVATE_MESSENGER_CLOSED')
|
|
129
|
+
if (timer !== undefined) clearTimer(timer)
|
|
130
|
+
for (const state of channels.values()) rejectJobs(state, closed)
|
|
131
|
+
channels.clear()
|
|
132
|
+
for (const wake of [...readyWaiters]) wake()
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -98,6 +98,12 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
98
98
|
for await (const item of queue.storedItems()) if (force || item.macAttempts < 3) records.push(item)
|
|
99
99
|
return records
|
|
100
100
|
},
|
|
101
|
+
current (item, { force = false } = {}) {
|
|
102
|
+
return serial(async () => {
|
|
103
|
+
const stored = await queue.getBy('byId', item.id)
|
|
104
|
+
return stored && (force || stored.macAttempts < 3) ? stored : null
|
|
105
|
+
})
|
|
106
|
+
},
|
|
101
107
|
recovered (item) {
|
|
102
108
|
return serial(async () => {
|
|
103
109
|
await queue.removeBy('byId', item.id)
|
|
@@ -10,6 +10,19 @@ ordinary availability recovery already requests a pass. Ciphertext stays in the
|
|
|
10
10
|
separate bounded FIFO until admitted to the normal inbox, evicted or explicitly
|
|
11
11
|
cleared. This does not retry application storage/permission failures as crypto.
|
|
12
12
|
|
|
13
|
+
Construction accepts `pendingDecryptionReady` (default `true`). Pass `false` to
|
|
14
|
+
hold pending queue reads/replay until `setPendingDecryptionReady(true)` releases
|
|
15
|
+
them after initial presentation. This never pauses live reception, sends or
|
|
16
|
+
channel preparation. Recovery waits for 250 ms of channel inactivity; a five-second
|
|
17
|
+
maximum admits one packet during sustained activity. It yields between records,
|
|
18
|
+
with at most four consecutive preferred records before another eligible channel.
|
|
19
|
+
`setPendingDecryptionPriority(peer | null)` retains a peer preference through
|
|
20
|
+
preparation; self-chat/`null` or removal clear it. Deadlines prevent preference
|
|
21
|
+
from starving other channels. Account unavailability/close still cancel recovery;
|
|
22
|
+
`force` bypasses only the MAC attempt limit. Awaiting the retry API awaits its
|
|
23
|
+
scheduled pass, so do not await it on the interface's critical startup path.
|
|
24
|
+
|
|
25
|
+
|
|
13
26
|
Provide `owner`, a byte-based NIP-44 v3 `signer` with `withSharedKey`, and either
|
|
14
27
|
`eventStore` or a `messageStorage.save(event, { peerPubkey, hearsay })` adapter.
|
|
15
28
|
Optional `chunkStorage` and `recoveryStorage` (`seeds`, `authorizations`) select
|
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
export function createForegroundWork () {
|
|
4
4
|
let active = 0
|
|
5
5
|
const waiters = new Set()
|
|
6
|
+
const observers = new Set()
|
|
7
|
+
const notify = () => { for (const listener of observers) listener(active > 0) }
|
|
6
8
|
const lifetime = new AbortController()
|
|
7
9
|
return {
|
|
8
10
|
enter () {
|
|
9
11
|
active++
|
|
12
|
+
notify()
|
|
10
13
|
let released = false
|
|
11
14
|
return () => {
|
|
12
15
|
if (released) return
|
|
13
16
|
released = true
|
|
14
17
|
if (--active === 0) for (const wake of [...waiters]) wake()
|
|
18
|
+
notify()
|
|
15
19
|
}
|
|
16
20
|
},
|
|
17
21
|
wait (signal) {
|
|
@@ -24,6 +28,7 @@ export function createForegroundWork () {
|
|
|
24
28
|
combined.addEventListener('abort', finish, { once: true })
|
|
25
29
|
})
|
|
26
30
|
},
|
|
27
|
-
|
|
31
|
+
observe (listener) { observers.add(listener); listener(active > 0); return () => observers.delete(listener) },
|
|
32
|
+
close () { lifetime.abort(); waiters.clear(); observers.clear() }
|
|
28
33
|
}
|
|
29
34
|
}
|
|
@@ -44,9 +44,11 @@ function messengerStatusSnapshot (value) {
|
|
|
44
44
|
return { closed: value.closed, paused: value.paused, pauseReasons: [...value.pauseReasons] }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
47
|
+
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, pendingDecryptionReady = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
48
48
|
fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
49
49
|
fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
|
|
50
|
+
if (typeof pendingDecryptionReady !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
51
|
+
let pendingDecryptionPeer = null
|
|
50
52
|
const reportedErrors = new WeakSet()
|
|
51
53
|
const reportError = error => {
|
|
52
54
|
if (error && (typeof error === 'object' || typeof error === 'function')) {
|
|
@@ -139,7 +141,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
139
141
|
let candidate
|
|
140
142
|
try {
|
|
141
143
|
candidate = await Messenger({
|
|
142
|
-
fallbackRelays, fallbackDelayMs, seedStorage: recoveryStorage?.seeds, userSigner, channels: [], useContentKeys, _waitForForeground: foreground.wait,
|
|
144
|
+
fallbackRelays, fallbackDelayMs, pendingDecryptionReady, seedStorage: recoveryStorage?.seeds, userSigner, channels: [], useContentKeys, _waitForForeground: foreground.wait, _observeForeground: foreground.observe,
|
|
143
145
|
onMessageQueued: () => { if (messengerBinding === binding) return drain() },
|
|
144
146
|
onStateChanged: state => {
|
|
145
147
|
if (closed || messengerBinding !== binding) return
|
|
@@ -152,6 +154,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
152
154
|
if (typeof candidate?.readStatus !== 'function' || !notified) throw new ValidationError('MESSENGER_STATE_CONTRACT_REQUIRED')
|
|
153
155
|
const status = messengerStatusSnapshot(candidate.readStatus())
|
|
154
156
|
messenger = candidate
|
|
157
|
+
messenger.setPendingDecryptionReady?.(pendingDecryptionReady)
|
|
158
|
+
messenger.setPendingDecryptionPriority?.(channels.get(pendingDecryptionPeer)?.pubkey ?? null)
|
|
155
159
|
stateChanged(status)
|
|
156
160
|
} catch (error) {
|
|
157
161
|
messengerBinding = null
|
|
@@ -168,6 +172,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
168
172
|
signal.throwIfAborted()
|
|
169
173
|
const channel = { signer: scoped, pubkey, readerPubkey: pubkey, mode, seeders: seedersForPeer(peer) }
|
|
170
174
|
channels.set(peer, channel)
|
|
175
|
+
if (peer === pendingDecryptionPeer) messenger?.setPendingDecryptionPriority?.(channel.pubkey)
|
|
171
176
|
await schedule()
|
|
172
177
|
signal.throwIfAborted()
|
|
173
178
|
return channel
|
|
@@ -558,6 +563,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
558
563
|
await writeDownloadIntent(() => downloads?.remove(id))
|
|
559
564
|
}
|
|
560
565
|
return {
|
|
566
|
+
setPendingDecryptionReady (ready) {
|
|
567
|
+
if (typeof ready !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
568
|
+
pendingDecryptionReady = ready
|
|
569
|
+
if (!closed) messenger?.setPendingDecryptionReady?.(ready)
|
|
570
|
+
},
|
|
571
|
+
setPendingDecryptionPriority (peer) {
|
|
572
|
+
if (peer !== null && !/^[0-9a-f]{64}$/.test(peer || '')) throw new ValidationError('INVALID_PENDING_DECRYPTION_PRIORITY')
|
|
573
|
+
pendingDecryptionPeer = peer === owner ? null : peer
|
|
574
|
+
if (!closed) messenger?.setPendingDecryptionPriority?.(channels.get(pendingDecryptionPeer)?.pubkey ?? null)
|
|
575
|
+
},
|
|
561
576
|
async retryPendingDecryptions (options) {
|
|
562
577
|
if (closed || !available) return
|
|
563
578
|
await ensureMessenger()
|
|
@@ -605,6 +620,10 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
605
620
|
if (next.size === peers.size && [...next].every(peer => peers.has(peer))) return configuring
|
|
606
621
|
peers.clear(); for (const peer of next) peers.add(peer)
|
|
607
622
|
preparation.reconcile(peers)
|
|
623
|
+
if (pendingDecryptionPeer && !peers.has(pendingDecryptionPeer)) {
|
|
624
|
+
pendingDecryptionPeer = null
|
|
625
|
+
messenger?.setPendingDecryptionPriority?.(null)
|
|
626
|
+
}
|
|
608
627
|
for (const peer of channels.keys()) {
|
|
609
628
|
if (!peers.has(peer)) {
|
|
610
629
|
channels.delete(peer); pendingPriorityRanges.delete(peer)
|