libp2r2p 0.11.21 → 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.
@@ -54,6 +54,10 @@ import { incompleteFetchError } from '../private-channel/helpers/fetch-error.js'
54
54
  import { recoveryRetryDelay, isPermanentRecoveryError } from './helpers/recovery-retry.js'
55
55
  import { normalizeRelayUrl } from '../url/index.js'
56
56
  import { deliveryInfo } from '../private-channel/helpers/rumor.js'
57
+ import { migrateMessageQueue } from './services/migrate-message-queue.js'
58
+ import { createPendingDecryption } from './services/pending-decryption.js'
59
+ import { createDecryptionScheduler } from './services/decryption-scheduler.js'
60
+ import { decryptionError, isDeferredDecryptionError } from '../private-channel/helpers/decryption-error.js'
57
61
  import { bytesToBase64 } from '../base64/index.js'
58
62
  import { ValidationError } from '../error/index.js'
59
63
  import { getRelaysByPubkey, pickRelaysForPubkeys, subscribeRelayListUpdates, relayPool } from '../relay/index.js'
@@ -233,6 +237,7 @@ export class PrivateMessenger {
233
237
  autoDeletionCapability = true,
234
238
  _indexedDB = globalThis.indexedDB,
235
239
  useContentKeys = true,
240
+ pendingDecryptionReady = true,
236
241
  onContentKeyChange,
237
242
  onMessageQueued,
238
243
  onStateChanged,
@@ -245,6 +250,8 @@ export class PrivateMessenger {
245
250
  _subscribeRelayListUpdates = subscribeRelayListUpdates,
246
251
  _isOnline = isOnline,
247
252
  _waitForForeground,
253
+ _observeForeground,
254
+ _now = Date.now,
248
255
  _onOnline = onOnline,
249
256
  _random = Math.random,
250
257
  _setTimeout = globalThis.setTimeout.bind(globalThis),
@@ -256,6 +263,7 @@ export class PrivateMessenger {
256
263
  _BroadcastChannel = _indexedDB === globalThis.indexedDB ? globalThis.BroadcastChannel : undefined
257
264
  } = {}) {
258
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')
259
267
  this.fallbackRelays = normalizeFallbackRelays(fallbackRelays)
260
268
  this.fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
261
269
  this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
@@ -380,6 +388,9 @@ export class PrivateMessenger {
380
388
  this.closePromise = null
381
389
  this.initSettledPromise = null
382
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))
383
394
  this.notifyStatus()
384
395
  }
385
396
 
@@ -419,6 +430,7 @@ export class PrivateMessenger {
419
430
  this.assertOpen()
420
431
  this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
421
432
  this.assertOpen()
433
+ await migrateMessageQueue(this._indexedDB, this.prefix)
422
434
  this.queue = await createQueue({
423
435
  prefix: this.prefix,
424
436
  indexes: MESSAGE_QUEUE_INDEXES,
@@ -427,6 +439,10 @@ export class PrivateMessenger {
427
439
  indexedDB: this._indexedDB
428
440
  })
429
441
  this.assertOpen()
442
+ this.pendingDecryption = await createPendingDecryption({
443
+ prefix: this.prefix, indexedDB: this._indexedDB,
444
+ report: detail => this.debug('pending-decryption', detail)
445
+ })
430
446
  this.seedQueue = this.seedStorage
431
447
  ? null
432
448
  : await createQueue({
@@ -446,16 +462,20 @@ export class PrivateMessenger {
446
462
  this.state = { channels: await this.stateStore.load() }
447
463
  await this.update({ userSigner, contentKeySigner, nymSigner: this.nymSigner, channels, relays, mode })
448
464
  this.initialized = true
465
+ this.retryPendingDecryptions().catch(error => this.onError?.(error))
449
466
  await this.refreshAndApplyStoragePolicy()
450
467
  this.broadcastStoragePolicyChange()
451
468
  return this
452
469
  } catch (err) {
470
+ this.decryptionRetry?.controller.abort(err)
471
+ await Promise.allSettled([this.decryptionRetry?.work])
453
472
  this.stopStorageMaintenance()
454
473
  this.stopStoragePolicyBroadcast()
455
474
  try { await this.queueOperationTail } catch {}
456
475
  try { await this.stateWriteTail } catch {}
457
476
  await Promise.allSettled([
458
477
  this.queue?.close?.(),
478
+ this.pendingDecryption?.close(),
459
479
  this.seedQueue?.close?.(),
460
480
  this.stateStore?.close?.()
461
481
  ])
@@ -1209,6 +1229,9 @@ export class PrivateMessenger {
1209
1229
  onMessage: message => this.receive(pubkey, () => this.handleMessage(pubkey, message), message),
1210
1230
  onSeed: seed => this.receive(pubkey, () => this.enqueueSeed(pubkey, seed), seed),
1211
1231
  onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
1232
+ onDecryptionFailure: failure => this.deferDecryption(failure),
1233
+ _onProcessing: pubkey => this.decryptionScheduler.activity(pubkey),
1234
+ isDecryptionPending: record => this.pendingDecryption.has(record),
1212
1235
  receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
1213
1236
  receivedChunkIndexedDB: this._indexedDB,
1214
1237
  onSubscriptionState: state => this.handleSubscriptionState(pubkey, state),
@@ -1241,6 +1264,11 @@ export class PrivateMessenger {
1241
1264
  continue
1242
1265
  }
1243
1266
  this.stopByChannel.set(pubkey, stop)
1267
+ this.decryptionReadyChannels ??= new Set()
1268
+ if (!this.decryptionReadyChannels.has(pubkey)) {
1269
+ this.decryptionReadyChannels.add(pubkey)
1270
+ this.retryPendingDecryptions().catch(error => this.onError?.(error))
1271
+ }
1244
1272
  this.watchRevisionByChannel.set(pubkey, revision)
1245
1273
  this.updateChannelState(pubkey, {
1246
1274
  lastWatchedAt: nowSeconds(),
@@ -1715,7 +1743,10 @@ export class PrivateMessenger {
1715
1743
  unwatch (channels) {
1716
1744
  const pubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.desiredChannels]
1717
1745
  for (const extension of this.extensions) extension.unwatch?.(pubkeys)
1718
- for (const pubkey of pubkeys) this.desiredChannels.delete(pubkey)
1746
+ for (const pubkey of pubkeys) {
1747
+ this.desiredChannels.delete(pubkey)
1748
+ this.decryptionScheduler.remove(pubkey)
1749
+ }
1719
1750
  for (const [key, pending] of this.pendingStorageWrites) if (pubkeys.includes(pending.record.channelPubkey)) this.pendingStorageWrites.delete(key)
1720
1751
  this.syncPauseRecovery()
1721
1752
  for (const pubkey of pubkeys) this.dropRecoveryState(pubkey)
@@ -1829,6 +1860,7 @@ export class PrivateMessenger {
1829
1860
  if (typeof reason !== 'string' || !reason.trim()) throw new ValidationError('PAUSE_REASON_REQUIRED')
1830
1861
  this.assertOpen()
1831
1862
  this.pauseReasons.add(reason)
1863
+ this.decryptionRetry?.controller.abort(this.pausedError())
1832
1864
  this.pauseRevisions.set(reason, (this.pauseRevisions.get(reason) || 0) + 1)
1833
1865
  this.sendRoutingPause.abort(this.pausedError())
1834
1866
  this.notifyStatus()
@@ -1865,6 +1897,7 @@ export class PrivateMessenger {
1865
1897
  }
1866
1898
  if (this.pauseReasons.size || this.closePromise) return
1867
1899
  const jobs = removed ? this.startChannelBackground(channels, { recover: true }) : [...this.channelBackground.values()].map(job => job.work)
1900
+ if (removed) this.retryPendingDecryptions().catch(error => this.onError?.(error))
1868
1901
  if (removed) {
1869
1902
  for (const extension of this.extensions) {
1870
1903
  const work = Promise.resolve().then(() => { if (!this.pauseReasons.size && !this.closePromise) return extension.resume?.() })
@@ -1883,6 +1916,7 @@ export class PrivateMessenger {
1883
1916
  if (job) { jobs.push(job.work); continue }
1884
1917
  const controller = new AbortController()
1885
1918
  const channel = this.channels.get(pubkey)
1919
+ const releaseActivity = this.decryptionScheduler.activity(pubkey)
1886
1920
  job = { controller, work: null }
1887
1921
  this.channelBackground.set(pubkey, job)
1888
1922
  const active = () => this.initialized && !controller.signal.aborted && !this.closePromise && !this.pauseReasons.size && this.channels.get(pubkey) === channel && this.desiredChannels.has(pubkey)
@@ -1919,6 +1953,7 @@ export class PrivateMessenger {
1919
1953
  throw error
1920
1954
  })
1921
1955
  job.work = Promise.race([work, cancelled]).finally(() => {
1956
+ releaseActivity()
1922
1957
  controller.signal.removeEventListener('abort', stopAbort)
1923
1958
  if (this.channelBackground.get(pubkey) === job) this.channelBackground.delete(pubkey)
1924
1959
  })
@@ -1928,18 +1963,19 @@ export class PrivateMessenger {
1928
1963
  return jobs
1929
1964
  }
1930
1965
 
1931
- receive (pubkey, operation, message = {}) {
1966
+ receive (pubkey, operation, message = {}, { trackActivity = true } = {}) {
1932
1967
  if (this.closePromise) {
1933
1968
  if (this.storageActive) this.recordInterruption([pubkey], messageTime(message))
1934
1969
  return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
1935
1970
  }
1971
+ const releaseActivity = trackActivity ? this.decryptionScheduler.activity(pubkey) : () => {}
1936
1972
  return this.queueIncoming(async () => {
1937
1973
  if (this.pauseReasons.size || !this.desiredChannels.has(pubkey)) {
1938
1974
  this.recordInterruption([pubkey], messageTime(message))
1939
1975
  throw this.pausedError()
1940
1976
  }
1941
1977
  return operation()
1942
- })
1978
+ }).finally(releaseActivity)
1943
1979
  }
1944
1980
 
1945
1981
  nip65WatchChannelPubkeys () {
@@ -2253,180 +2289,204 @@ export class PrivateMessenger {
2253
2289
 
2254
2290
  async ask ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
2255
2291
  const channel = this.requireWritableChannel(channelPubkey)
2256
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [receiverPubkey], relays, relayToReceivers })
2257
- this.debugSend('ask', channelPubkey, { code, receiverPubkey })
2258
- return this._privateMessage.ask({
2259
- senderSigner: this.userSigner,
2260
- imkcSigner: this.contentKeySigner,
2261
- privateChannelSigner: channel.signer,
2262
- privateChannelReaderPubkey: channel.readerPubkey,
2263
- receiverPubkey,
2264
- ...routing,
2265
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2266
- temporaryStorageArea: this.temporaryStorageArea,
2267
- deletionPubkey,
2268
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2269
- message,
2270
- code,
2271
- payload,
2272
- error,
2273
- content,
2274
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2275
- _getIykcProofs: this.contentKeyLookup()
2276
- })
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() }
2277
2316
  }
2278
2317
 
2279
2318
  async reply ({ channelPubkey = this.defaultChannelPubkey(), question, receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
2280
2319
  const channel = this.requireWritableChannel(channelPubkey)
2281
- const resolvedReceiverPubkey = receiverPubkey || question?.pubkey || ''
2282
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [resolvedReceiverPubkey], relays, relayToReceivers })
2283
- this.debugSend('reply', channelPubkey, { code, receiverPubkey: receiverPubkey || question?.pubkey || '' })
2284
- return this._privateMessage.reply({
2285
- senderSigner: this.userSigner,
2286
- imkcSigner: this.contentKeySigner,
2287
- privateChannelSigner: channel.signer,
2288
- privateChannelReaderPubkey: channel.readerPubkey,
2289
- question,
2290
- receiverPubkey,
2291
- ...routing,
2292
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2293
- temporaryStorageArea: this.temporaryStorageArea,
2294
- deletionPubkey,
2295
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2296
- message,
2297
- code,
2298
- payload,
2299
- error,
2300
- content,
2301
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2302
- _getIykcProofs: this.contentKeyLookup()
2303
- })
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() }
2304
2346
  }
2305
2347
 
2306
2348
  async tell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
2307
2349
  const channel = this.requireWritableChannel(channelPubkey)
2308
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys: [receiverPubkey], relays, relayToReceivers })
2309
- this.debugSend('tell', channelPubkey, { code, receiverPubkey })
2310
- return this._privateMessage.tell({
2311
- senderSigner: this.userSigner,
2312
- imkcSigner: this.contentKeySigner,
2313
- privateChannelSigner: channel.signer,
2314
- privateChannelReaderPubkey: channel.readerPubkey,
2315
- receiverPubkey,
2316
- ...routing,
2317
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2318
- temporaryStorageArea: this.temporaryStorageArea,
2319
- deletionPubkey,
2320
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2321
- message,
2322
- code,
2323
- payload,
2324
- error,
2325
- content,
2326
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2327
- _getIykcProofs: this.contentKeyLookup()
2328
- })
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() }
2329
2374
  }
2330
2375
 
2331
2376
  async yell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
2332
2377
  const channel = this.requireWritableChannel(channelPubkey)
2333
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
2334
- this.debugSend('yell', channelPubkey, { code, receiverPubkeys })
2335
- return this._privateMessage.yell({
2336
- senderSigner: this.userSigner,
2337
- imkcSigner: this.contentKeySigner,
2338
- privateChannelSigner: channel.signer,
2339
- privateChannelReaderPubkey: channel.readerPubkey,
2340
- receiverPubkeys,
2341
- ...routing,
2342
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2343
- temporaryStorageArea: this.temporaryStorageArea,
2344
- deletionPubkey,
2345
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2346
- message,
2347
- code,
2348
- payload,
2349
- error,
2350
- content,
2351
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2352
- _getIykcProofs: this.contentKeyLookup()
2353
- })
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() }
2354
2402
  }
2355
2403
 
2356
2404
  async broadcastRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, deletionPubkey, signal }) {
2357
2405
  const channel = this.requireWritableChannel(channelPubkey)
2358
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers, signal })
2359
- this.debugSend('broadcastRumor', channelPubkey, { receiverPubkeys })
2360
- return this._privateMessage.broadcastRumor({
2361
- senderSigner: this.userSigner,
2362
- imkcSigner: this.contentKeySigner,
2363
- privateChannelSigner: channel.signer,
2364
- privateChannelReaderPubkey: channel.readerPubkey,
2365
- receiverPubkeys,
2366
- ...routing,
2367
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2368
- temporaryStorageArea: this.temporaryStorageArea,
2369
- deletionPubkey,
2370
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2371
- rumor,
2372
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2373
- _getIykcProofs: this.contentKeyLookup()
2374
- })
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() }
2375
2426
  }
2376
2427
 
2377
2428
  async broadcastEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, deletionPubkey, signal }) {
2378
2429
  const channel = this.requireWritableChannel(channelPubkey)
2379
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers, signal })
2380
- this.debugSend('broadcastEvent', channelPubkey, { receiverPubkeys })
2381
- return this._privateMessage.broadcastEvent({
2382
- senderSigner: this.userSigner,
2383
- imkcSigner: this.contentKeySigner,
2384
- privateChannelSigner: channel.signer,
2385
- privateChannelReaderPubkey: channel.readerPubkey,
2386
- receiverPubkeys,
2387
- ...routing,
2388
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2389
- temporaryStorageArea: this.temporaryStorageArea,
2390
- deletionPubkey,
2391
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2392
- event,
2393
- onPreparedSeed: this.outgoingSeedHandler(channelPubkey),
2394
- _getIykcProofs: this.contentKeyLookup()
2395
- })
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() }
2396
2450
  }
2397
2451
 
2398
2452
  async broadcastNymRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, nymSigner, deletionPubkey }) {
2399
2453
  const channel = this.requireWritableChannel(channelPubkey)
2400
- const resolvedNymSigner = this.requireNymSigner(channel, nymSigner)
2401
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
2402
- this.debugSend('broadcastNymRumor', channelPubkey, { receiverPubkeys })
2403
- return this._privateMessage.broadcastNymRumor({
2404
- nymSigner: resolvedNymSigner,
2405
- privateChannelSigner: channel.signer,
2406
- privateChannelReaderPubkey: channel.readerPubkey,
2407
- ...routing,
2408
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2409
- deletionPubkey,
2410
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2411
- rumor
2412
- })
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() }
2413
2470
  }
2414
2471
 
2415
2472
  async broadcastNymEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, nymSigner, deletionPubkey }) {
2416
2473
  const channel = this.requireWritableChannel(channelPubkey)
2417
- const resolvedNymSigner = this.requireNymSigner(channel, nymSigner)
2418
- const routing = await this.resolveSendRouting({ channel, receiverPubkeys, relays, relayToReceivers })
2419
- this.debugSend('broadcastNymEvent', channelPubkey, { receiverPubkeys })
2420
- return this._privateMessage.broadcastNymEvent({
2421
- nymSigner: resolvedNymSigner,
2422
- privateChannelSigner: channel.signer,
2423
- privateChannelReaderPubkey: channel.readerPubkey,
2424
- ...routing,
2425
- expirationSeconds: this.eventExpirationSecondsFor(channel),
2426
- deletionPubkey,
2427
- autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
2428
- event
2429
- })
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() }
2430
2490
  }
2431
2491
 
2432
2492
  async publishSeederPresence (channelPubkey = this.defaultChannelPubkey(), { signal } = {}) {
@@ -2739,7 +2799,19 @@ export class PrivateMessenger {
2739
2799
  for (const line of splitJsonl(jsonl)) {
2740
2800
  const record = parseJson(line, null)
2741
2801
  if (!record) continue
2742
- const recovered = await this.messageFromBackfillRecord(channelPubkey, record)
2802
+ const pendingRecord = record.recordType === ROUTER_SEED_RECORD_TYPE && isPrivateChannelRouter(record.router)
2803
+ ? { format: 'routerRow_v1', channelPubkey, router: record.router, outer: { id: '', created_at: record.lastSeenAt || record.router.created_at } }
2804
+ : null
2805
+ if (pendingRecord && await this.pendingDecryption.has(pendingRecord)) continue
2806
+ let recovered
2807
+ try { recovered = await this.messageFromBackfillRecord(channelPubkey, record) } catch (cause) {
2808
+ const error = decryptionError(cause, { channelPubkey })
2809
+ if (!isDeferredDecryptionError(error) || record.recordType !== ROUTER_SEED_RECORD_TYPE) throw error
2810
+ await this.deferDecryption({
2811
+ error, record: pendingRecord
2812
+ })
2813
+ continue
2814
+ }
2743
2815
  if (!recovered) continue
2744
2816
  await this.enqueueRumor(recovered.type, channelPubkey, {
2745
2817
  event: recovered.event,
@@ -2847,6 +2919,9 @@ export class PrivateMessenger {
2847
2919
  onNymEvent: (event, outer, meta) => this.receive(channelPubkey, () => this.enqueueRumor('nym', channelPubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }),
2848
2920
  onSeedEvent: seed => this.receive(channelPubkey, () => this.enqueueSeed(channelPubkey, seed), seed),
2849
2921
  onContentKeyUsage: usage => this.handleContentKeyUsage(channelPubkey, usage),
2922
+ onDecryptionFailure: failure => this.deferDecryption(failure),
2923
+ _onProcessing: pubkey => this.decryptionScheduler.activity(pubkey),
2924
+ isDecryptionPending: record => this.pendingDecryption.has(record),
2850
2925
  onError: err => { throw err }
2851
2926
  }
2852
2927
  }
@@ -3085,9 +3160,11 @@ export class PrivateMessenger {
3085
3160
  this.dropRecoveryState(pubkey)
3086
3161
  return this.runQueueOperation(async () => {
3087
3162
  this.channels.delete(pubkey)
3163
+ this.decryptionReadyChannels?.delete(pubkey)
3088
3164
  this.removeChannelState(pubkey)
3089
3165
  await this.flushStateWrites()
3090
3166
  await this.queue.removeBy('byChannel', pubkey)
3167
+ await this.pendingDecryption?.clearChannel(pubkey)
3091
3168
  await (this.seedStorage ? this.seedStorage.removeLocal({ channelPubkey: pubkey }) : this.seedQueue.removeBy('byChannel', pubkey))
3092
3169
  await this.touchStorageActivity({ force: true })
3093
3170
  this.ensureRelayListWatcher()
@@ -3098,6 +3175,112 @@ export class PrivateMessenger {
3098
3175
  return this.runQueueOperation(() => this.queue.clear())
3099
3176
  }
3100
3177
 
3178
+ async deferDecryption (failure) {
3179
+ this.assertOpen()
3180
+ const pubkey = failure.record.channelPubkey
3181
+ if (this.pauseReasons.size || !this.channels.has(pubkey)) throw this.pausedError()
3182
+ try { return await this.pendingDecryption.save(failure) } catch (error) {
3183
+ this.recordInterruption([pubkey], failure.record.outer?.created_at)
3184
+ if (isRecoverableStorageFailure(error)) this.pauseInternally('storage').catch(err => this.onError?.(err))
3185
+ throw error
3186
+ }
3187
+ }
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
+
3200
+ retryPendingDecryptions ({ force = false } = {}) {
3201
+ if (typeof force !== 'boolean') throw new ValidationError('INVALID_FORCE')
3202
+ if (this.closePromise || this.pauseReasons.size || !this.initialized) return Promise.resolve()
3203
+ if (this.decryptionRetry && !this.decryptionRetry.controller.signal.aborted) {
3204
+ // Before a pass reads its snapshot, new requests belong to that pass.
3205
+ if (this.decryptionRetry.started) this.decryptionRetry.again = true
3206
+ this.decryptionRetry.force ||= force
3207
+ return this.decryptionRetry.work
3208
+ }
3209
+ const job = { controller: new AbortController(), again: false, force, started: false, work: null }
3210
+ const previous = this.decryptionRetry?.work
3211
+ this.decryptionRetry = job
3212
+ const { signal } = job.controller
3213
+ const run = async () => {
3214
+ do {
3215
+ await this.decryptionScheduler.waitReady(signal)
3216
+ signal.throwIfAborted()
3217
+ job.again = false
3218
+ job.started = true
3219
+ const force = job.force
3220
+ job.force = false
3221
+ const groups = new Map()
3222
+ for (const record of await this.pendingDecryption.records({ force })) {
3223
+ if (!groups.has(record.channelPubkey)) groups.set(record.channelPubkey, [])
3224
+ groups.get(record.channelPubkey).push(record)
3225
+ }
3226
+ const results = await Promise.allSettled([...groups].map(async ([channelPubkey, records]) => {
3227
+ const channel = this.channels.get(channelPubkey)
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
3274
+ } while (job.again)
3275
+ }
3276
+ job.work = Promise.resolve(previous).catch(() => {}).then(run).catch(error => {
3277
+ if (!signal.aborted) throw error
3278
+ }).finally(() => {
3279
+ if (this.decryptionRetry === job) this.decryptionRetry = null
3280
+ })
3281
+ return job.work
3282
+ }
3283
+
3101
3284
  async cleanupStaleChannels ({ storageSnapshot } = {}) {
3102
3285
  if (!this.prefix) return
3103
3286
  storageSnapshot ||= await this.readStoragePolicySnapshot()
@@ -3150,6 +3333,9 @@ export class PrivateMessenger {
3150
3333
  }
3151
3334
 
3152
3335
  close () {
3336
+ this.decryptionRetry?.controller.abort(new Error('PRIVATE_MESSENGER_CLOSED'))
3337
+ this.decryptionScheduler.close()
3338
+ this.stopDecryptionForeground?.()
3153
3339
  if (this.closePromise) return this.closePromise
3154
3340
  const initSettledPromise = this.initSettledPromise
3155
3341
  this.sendRoutingLifetime.abort()
@@ -3189,11 +3375,13 @@ export class PrivateMessenger {
3189
3375
  await Promise.allSettled([...this.deliveryReads])
3190
3376
  await Promise.all([...this.deliveries].map(delivery => delivery.nack()))
3191
3377
  await this.stateWriteTail
3378
+ await Promise.allSettled([this.decryptionRetry?.work])
3192
3379
  try { await this.storageTouchPromise } catch {}
3193
3380
  await this.storageMaintenancePromise
3194
3381
  try { await this.storagePolicyRefreshTail } catch {}
3195
3382
  await Promise.all([
3196
3383
  this.queue?.close?.(),
3384
+ this.pendingDecryption?.close(),
3197
3385
  this.seedQueue?.close?.(),
3198
3386
  this.stateStore?.close?.()
3199
3387
  ])