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.
- package/README.md +72 -0
- package/content-key/index.js +1 -1
- package/content-key/services/iykc-proof.js +41 -2
- package/package.json +1 -1
- package/private-channel/helpers/decryption-error.js +15 -0
- package/private-channel/index.js +153 -27
- package/private-message/index.js +5 -1
- package/private-messenger/index.js +338 -150
- package/private-messenger/services/decryption-scheduler.js +135 -0
- package/private-messenger/services/migrate-message-queue.js +95 -0
- package/private-messenger/services/pending-decryption.js +126 -0
- package/private-messenger/services/storage-maintenance.js +1 -0
- package/private-messenger/session/README.md +19 -0
- package/private-messenger/session/helpers/foreground-work.js +6 -1
- package/private-messenger/session/index.js +26 -2
|
@@ -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)
|
|
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
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
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
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
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
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
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
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
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
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
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
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
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
|
|
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
|
])
|