libp2r2p 0.10.18 → 0.10.20
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 +193 -35
- package/content-key/services/iykc-proof.js +1 -1
- package/idb-queue/index.js +66 -2
- package/nip46/services/client.js +5 -1
- package/nip46/services/transport.js +4 -2
- package/package.json +1 -1
- package/private-channel/helpers/rumor.js +26 -0
- package/private-channel/index.js +28 -14
- package/private-channel/services/received-chunks.js +3 -2
- package/private-message/index.js +333 -281
- package/private-messenger/index.js +328 -91
- package/private-messenger/services/channel-state.js +1 -1
- package/relay/services/events.js +1 -1
- package/relay/services/query.js +5 -2
- package/relay/services/relay-pool.js +161 -127
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
// - Split reader channel: { signer, readerPubkey } signs as the channel key but encrypts/decrypts the outer router with the reader pubkey.
|
|
16
16
|
// - Reader-secret channel: { signer, readerSigner } is also valid; the reader signer decrypts the router.
|
|
17
17
|
// - Reader-only channel: { pubkey, readerSigner } can watch/fetch/drain messages but cannot send or seed recovery replies.
|
|
18
|
-
// for await (const
|
|
18
|
+
// for await (const { message, ack } of messenger.messages()) {
|
|
19
|
+
// await persistPrivateMessage(message)
|
|
20
|
+
// await ack()
|
|
21
|
+
// }
|
|
19
22
|
// await messenger.ask({ receiverPubkey, payload: { ping: true } })
|
|
20
23
|
// await messenger.reply({ question: msg.question, payload: { ok: true } })
|
|
21
24
|
// await messenger.tell({ receiverPubkey, payload: { note: 'hello' } })
|
|
@@ -29,7 +32,7 @@
|
|
|
29
32
|
//
|
|
30
33
|
// Missed-message recovery:
|
|
31
34
|
// - Each watched channel stores lastSeenAt/lastWatchedAt in IndexedDB.
|
|
32
|
-
// -
|
|
35
|
+
// - First watch scans the recovery window; reload resumes persisted gaps/checkpoints.
|
|
33
36
|
// - Browser offline/online events add explicit offline ranges with a small skew.
|
|
34
37
|
// - Recovery defaults to 7 days and can be overridden per channel; zero disables durable recovery.
|
|
35
38
|
// - Channel state not actively leased or watched within the configured stale window is pruned.
|
|
@@ -40,6 +43,7 @@
|
|
|
40
43
|
// - For other event-list replies, use createEventReplyPacker({ messenger, question, code }).update(event).
|
|
41
44
|
|
|
42
45
|
import * as privateMessage from '../private-message/index.js'
|
|
46
|
+
import { deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
43
47
|
import { bytesToBase64 } from '../base64/index.js'
|
|
44
48
|
import { ValidationError } from '../error/index.js'
|
|
45
49
|
import { getRelaysByPubkey, pickRelaysForPubkeys, subscribeRelayListUpdates } from '../relay/index.js'
|
|
@@ -100,7 +104,7 @@ const SEED_TIME = '__p2r2pSeedTime'
|
|
|
100
104
|
const MESSAGE_QUEUE_INDEXES = {
|
|
101
105
|
byChannel: 'channelPubkey',
|
|
102
106
|
byChannelTypeEventId: {
|
|
103
|
-
keyPath: ['channelPubkey', 'type', 'event.id'],
|
|
107
|
+
keyPath: ['channelPubkey', 'type', 'event.id', 'provenance'],
|
|
104
108
|
unique: true
|
|
105
109
|
}
|
|
106
110
|
}
|
|
@@ -236,7 +240,7 @@ export class PrivateMessenger {
|
|
|
236
240
|
this.onMessageQueued = onMessageQueued
|
|
237
241
|
this.onDebug = onDebug
|
|
238
242
|
this.onError = onError
|
|
239
|
-
this._privateMessage = _privateMessage
|
|
243
|
+
this._privateMessage = _privateMessage.createPrivateMessageSession?.() ?? _privateMessage
|
|
240
244
|
this._privateChannel = _privateChannel
|
|
241
245
|
this._getRelaysByPubkey = _getRelaysByPubkey
|
|
242
246
|
this._pickRelaysForPubkeys = _pickRelaysForPubkeys
|
|
@@ -260,8 +264,20 @@ export class PrivateMessenger {
|
|
|
260
264
|
this.stateStore = null
|
|
261
265
|
this.state = { channels: {} }
|
|
262
266
|
this.stateWriteTail = Promise.resolve()
|
|
267
|
+
this.stateWriteError = null
|
|
263
268
|
this.channels = new Map()
|
|
264
269
|
this.stopByChannel = new Map()
|
|
270
|
+
this.desiredChannels = new Set()
|
|
271
|
+
this.pauseReasons = new Set()
|
|
272
|
+
this.deliveries = new Set()
|
|
273
|
+
this.deliveryReads = new Set()
|
|
274
|
+
this.deliveryWaiters = new Set()
|
|
275
|
+
this.recoveries = new Map()
|
|
276
|
+
this.recoveryControllers = new Set()
|
|
277
|
+
this.resumeWork = null
|
|
278
|
+
this.capacityTimer = null
|
|
279
|
+
this.capacityCheck = null
|
|
280
|
+
this.capacityRequiredBytes = 0
|
|
265
281
|
this.reloadGapTimers = new Map()
|
|
266
282
|
this.watchRevisionByChannel = new Map()
|
|
267
283
|
this.presenceTimers = new Map()
|
|
@@ -327,7 +343,7 @@ export class PrivateMessenger {
|
|
|
327
343
|
prefix: this.prefix,
|
|
328
344
|
indexes: MESSAGE_QUEUE_INDEXES,
|
|
329
345
|
maxBytes: this.messageQueueMaxBytes,
|
|
330
|
-
evictionPolicy: '
|
|
346
|
+
evictionPolicy: 'reject',
|
|
331
347
|
indexedDB: this._indexedDB
|
|
332
348
|
})
|
|
333
349
|
this.assertOpen()
|
|
@@ -453,9 +469,9 @@ export class PrivateMessenger {
|
|
|
453
469
|
const channels = [...this.channels.values()]
|
|
454
470
|
await this.applyRecoveryPolicies(channels)
|
|
455
471
|
await this.cleanupStaleChannels()
|
|
456
|
-
const pubkeys = [...this.
|
|
472
|
+
const pubkeys = [...this.desiredChannels]
|
|
457
473
|
if (pubkeys.length) {
|
|
458
|
-
await this.
|
|
474
|
+
await this.stopWatches(pubkeys)
|
|
459
475
|
await this.watch(pubkeys)
|
|
460
476
|
}
|
|
461
477
|
await this.reconcilePresencePublishers()
|
|
@@ -560,7 +576,7 @@ export class PrivateMessenger {
|
|
|
560
576
|
}
|
|
561
577
|
|
|
562
578
|
queueIncoming (operation) {
|
|
563
|
-
return this.runQueueOperation(operation)
|
|
579
|
+
return this.runQueueOperation(operation)
|
|
564
580
|
}
|
|
565
581
|
|
|
566
582
|
debug (action, detail = {}) {
|
|
@@ -615,6 +631,7 @@ export class PrivateMessenger {
|
|
|
615
631
|
const nextChannels = await this.normalizeChannels(channels, { relays, mode })
|
|
616
632
|
this.assertOpen()
|
|
617
633
|
const nextPubkeys = new Set(nextChannels.map(channel => channel.pubkey))
|
|
634
|
+
const watchPubkeys = [...nextPubkeys].filter(pubkey => !this.channels.has(pubkey) || this.desiredChannels.has(pubkey))
|
|
618
635
|
const removedPubkeys = [...this.channels.keys()].filter(pubkey => !nextPubkeys.has(pubkey))
|
|
619
636
|
const updatesStoragePolicy = updatesStalePolicy || updatesIdentityPolicy
|
|
620
637
|
|
|
@@ -652,7 +669,7 @@ export class PrivateMessenger {
|
|
|
652
669
|
|
|
653
670
|
await this.cleanupStaleChannels({ storageSnapshot })
|
|
654
671
|
await this.applyRecoveryPolicies(nextChannels)
|
|
655
|
-
await this.watch(
|
|
672
|
+
await this.watch(watchPubkeys)
|
|
656
673
|
await this.reconcilePresencePublishers()
|
|
657
674
|
if (this.storagePolicyRevision === storageSnapshot.policyRevision) {
|
|
658
675
|
this.storagePolicyNeedsApply = false
|
|
@@ -760,6 +777,7 @@ export class PrivateMessenger {
|
|
|
760
777
|
const snapshot = structuredClone(changed)
|
|
761
778
|
const write = this.stateWriteTail.then(() => this.stateStore.update(snapshot, removed))
|
|
762
779
|
this.stateWriteTail = write.catch(err => {
|
|
780
|
+
this.stateWriteError = err
|
|
763
781
|
try { this.onError?.(err) } catch {}
|
|
764
782
|
})
|
|
765
783
|
return write
|
|
@@ -767,6 +785,15 @@ export class PrivateMessenger {
|
|
|
767
785
|
|
|
768
786
|
async flushStateWrites () {
|
|
769
787
|
await this.stateWriteTail
|
|
788
|
+
if (!this.stateWriteError) return
|
|
789
|
+
const repair = this.stateWriteTail.then(async () => {
|
|
790
|
+
const stored = await this.stateStore.load()
|
|
791
|
+
const snapshot = structuredClone(this.state.channels)
|
|
792
|
+
await this.stateStore.update(snapshot, Object.keys(stored).filter(pubkey => !Object.hasOwn(snapshot, pubkey)))
|
|
793
|
+
this.stateWriteError = null
|
|
794
|
+
})
|
|
795
|
+
this.stateWriteTail = repair.catch(err => { this.stateWriteError = err })
|
|
796
|
+
await repair
|
|
770
797
|
}
|
|
771
798
|
|
|
772
799
|
async stampChannelActivity (pubkeys) {
|
|
@@ -782,6 +809,7 @@ export class PrivateMessenger {
|
|
|
782
809
|
}
|
|
783
810
|
const write = this.stateWriteTail.then(() => this.stateStore.touch(pubkeys, lastWatchedAt))
|
|
784
811
|
this.stateWriteTail = write.catch(err => {
|
|
812
|
+
this.stateWriteError = err
|
|
785
813
|
try { this.onError?.(err) } catch {}
|
|
786
814
|
})
|
|
787
815
|
await write
|
|
@@ -811,6 +839,7 @@ export class PrivateMessenger {
|
|
|
811
839
|
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
812
840
|
const write = this.stateWriteTail.then(() => this.stateStore.update({}, pubkeys))
|
|
813
841
|
this.stateWriteTail = write.catch(err => {
|
|
842
|
+
this.stateWriteError = err
|
|
814
843
|
try { this.onError?.(err) } catch {}
|
|
815
844
|
})
|
|
816
845
|
return write
|
|
@@ -865,7 +894,8 @@ export class PrivateMessenger {
|
|
|
865
894
|
}
|
|
866
895
|
|
|
867
896
|
trackSeederActivity (channelPubkey, message) {
|
|
868
|
-
|
|
897
|
+
if (message.provenance === 'hearsay') return false
|
|
898
|
+
const senderPubkey = message.senderPubkey || message.event?.pubkey
|
|
869
899
|
if (!senderPubkey) return false
|
|
870
900
|
|
|
871
901
|
const channel = this.channels.get(channelPubkey)
|
|
@@ -955,7 +985,7 @@ export class PrivateMessenger {
|
|
|
955
985
|
start: Math.max(0, Math.floor(start)),
|
|
956
986
|
end: Math.floor(end)
|
|
957
987
|
}
|
|
958
|
-
if (normalized.end
|
|
988
|
+
if (normalized.end < normalized.start || normalized.end < minStart) return
|
|
959
989
|
normalized.start = Math.max(normalized.start, minStart)
|
|
960
990
|
|
|
961
991
|
const state = this.readState()
|
|
@@ -969,12 +999,12 @@ export class PrivateMessenger {
|
|
|
969
999
|
this.writeState(state)
|
|
970
1000
|
}
|
|
971
1001
|
|
|
972
|
-
closeOpenOfflineRanges () {
|
|
1002
|
+
closeOpenOfflineRanges (channels = [...this.desiredChannels]) {
|
|
973
1003
|
const state = this.readState()
|
|
974
1004
|
const end = nowSeconds()
|
|
975
|
-
for (const pubkey of
|
|
1005
|
+
for (const pubkey of channels) {
|
|
976
1006
|
const current = state.channels[pubkey]
|
|
977
|
-
if (
|
|
1007
|
+
if (current?.openOfflineStart == null) continue
|
|
978
1008
|
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
979
1009
|
if (!recoverySeconds) {
|
|
980
1010
|
delete current.openOfflineStart
|
|
@@ -984,7 +1014,7 @@ export class PrivateMessenger {
|
|
|
984
1014
|
}
|
|
985
1015
|
const minStart = end - recoverySeconds
|
|
986
1016
|
const start = Math.max(minStart, Math.max(0, current.openOfflineStart))
|
|
987
|
-
if (end
|
|
1017
|
+
if (end >= start) {
|
|
988
1018
|
current.offlineRanges = mergeRanges((current.offlineRanges || []).concat([{ start, end }]))
|
|
989
1019
|
}
|
|
990
1020
|
delete current.openOfflineStart
|
|
@@ -996,11 +1026,28 @@ export class PrivateMessenger {
|
|
|
996
1026
|
async watch (channels = [...this.channels.keys()], { scheduleReloadGap = true } = {}) {
|
|
997
1027
|
this.assertOpen()
|
|
998
1028
|
const channelPubkeys = uniq(channels)
|
|
1029
|
+
for (const pubkey of channelPubkeys) {
|
|
1030
|
+
this.requireChannel(pubkey)
|
|
1031
|
+
this.desiredChannels.add(pubkey)
|
|
1032
|
+
}
|
|
1033
|
+
this.ensureNetworkWatchers()
|
|
1034
|
+
if (this.pauseReasons.size) {
|
|
1035
|
+
this.recordInterruption(channelPubkeys)
|
|
1036
|
+
await this.flushStateWrites()
|
|
1037
|
+
return this
|
|
1038
|
+
}
|
|
999
1039
|
for (const pubkey of channelPubkeys) {
|
|
1000
1040
|
const channel = this.channels.get(pubkey)
|
|
1001
1041
|
if (!channel) throw new ValidationError('UNKNOWN_CHANNEL')
|
|
1042
|
+
const revision = (this.watchRevisionByChannel.get(pubkey) || 0) + 1
|
|
1043
|
+
this.watchRevisionByChannel.set(pubkey, revision)
|
|
1044
|
+
// Persist the pending interval before live delivery can advance lastSeenAt.
|
|
1045
|
+
// Neither a crash nor a newly received message may erase the initial scan.
|
|
1046
|
+
this.recordRecoveryWindow(pubkey)
|
|
1047
|
+
await this.flushStateWrites()
|
|
1002
1048
|
const watchRelays = await this.resolveWatchRelays(channel)
|
|
1003
1049
|
this.assertOpen()
|
|
1050
|
+
if (this.pauseReasons.size || !this.desiredChannels.has(pubkey) || revision !== (this.watchRevisionByChannel.get(pubkey) || 0)) continue
|
|
1004
1051
|
const stop = await this._privateMessage.watch({
|
|
1005
1052
|
channels: [pubkey],
|
|
1006
1053
|
relays: watchRelays,
|
|
@@ -1010,24 +1057,25 @@ export class PrivateMessenger {
|
|
|
1010
1057
|
privateChannelReaderSigner: channel.readerSigner,
|
|
1011
1058
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
1012
1059
|
mode: channel.mode,
|
|
1013
|
-
onAsk: message => this.
|
|
1014
|
-
onReply: message => this.
|
|
1015
|
-
onTell: message => this.
|
|
1016
|
-
onYell: message => this.
|
|
1017
|
-
onNym: message => this.
|
|
1018
|
-
onMessage: message => this.
|
|
1019
|
-
onSeed: seed => this.
|
|
1060
|
+
onAsk: message => this.receive(pubkey, () => this.handleAsk(pubkey, message), message),
|
|
1061
|
+
onReply: message => this.receive(pubkey, () => this.handleReply(pubkey, message), message),
|
|
1062
|
+
onTell: message => this.receive(pubkey, () => this.handleTell(pubkey, message), message),
|
|
1063
|
+
onYell: message => this.receive(pubkey, () => this.handleYell(pubkey, message), message),
|
|
1064
|
+
onNym: message => this.receive(pubkey, () => this.handleNym(pubkey, message), message),
|
|
1065
|
+
onMessage: message => this.receive(pubkey, () => this.handleMessage(pubkey, message), message),
|
|
1066
|
+
onSeed: seed => this.receive(pubkey, () => this.enqueueSeed(pubkey, seed), seed),
|
|
1020
1067
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
1021
1068
|
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1022
1069
|
receivedChunkIndexedDB: this._indexedDB,
|
|
1023
1070
|
onError: err => this.onError?.(err)
|
|
1024
1071
|
})
|
|
1025
|
-
if (this.closePromise) {
|
|
1072
|
+
if (this.closePromise || this.pauseReasons.size || !this.desiredChannels.has(pubkey) || revision !== (this.watchRevisionByChannel.get(pubkey) || 0)) {
|
|
1026
1073
|
await stop?.()
|
|
1027
1074
|
this.assertOpen()
|
|
1075
|
+
continue
|
|
1028
1076
|
}
|
|
1029
1077
|
this.stopByChannel.set(pubkey, stop)
|
|
1030
|
-
this.watchRevisionByChannel.set(pubkey,
|
|
1078
|
+
this.watchRevisionByChannel.set(pubkey, revision)
|
|
1031
1079
|
this.updateChannelState(pubkey, {
|
|
1032
1080
|
lastWatchedAt: nowSeconds(),
|
|
1033
1081
|
mode: channel.mode,
|
|
@@ -1042,21 +1090,35 @@ export class PrivateMessenger {
|
|
|
1042
1090
|
seeders: channel.seeders,
|
|
1043
1091
|
seederCount: channel.seeders.length
|
|
1044
1092
|
})
|
|
1045
|
-
if (scheduleReloadGap)
|
|
1093
|
+
if (scheduleReloadGap) {
|
|
1094
|
+
this.closeOpenOfflineRanges([pubkey])
|
|
1095
|
+
this.scheduleReloadGap(pubkey)
|
|
1096
|
+
}
|
|
1046
1097
|
}
|
|
1047
1098
|
this.ensureNetworkWatchers()
|
|
1048
1099
|
this.ensureRelayListWatcher()
|
|
1049
1100
|
return this
|
|
1050
1101
|
}
|
|
1051
1102
|
|
|
1052
|
-
|
|
1053
|
-
const
|
|
1103
|
+
recordInterruption (channels, at = nowSeconds()) {
|
|
1104
|
+
const state = this.readState()
|
|
1105
|
+
for (const pubkey of channels) {
|
|
1106
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) continue
|
|
1107
|
+
const current = state.channels[pubkey] || {}
|
|
1108
|
+
const start = Math.max(0, at - this.offlineSkewSeconds)
|
|
1109
|
+
current.openOfflineStart = Math.min(current.openOfflineStart ?? start, start)
|
|
1110
|
+
state.channels[pubkey] = current
|
|
1111
|
+
}
|
|
1112
|
+
this.writeState(state)
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
stopWatches (channels = [...this.stopByChannel.keys()]) {
|
|
1054
1116
|
const closing = []
|
|
1055
|
-
for (const pubkey of
|
|
1117
|
+
for (const pubkey of channels) {
|
|
1056
1118
|
this.cancelReloadGap(pubkey)
|
|
1057
1119
|
this.watchRevisionByChannel.set(pubkey, (this.watchRevisionByChannel.get(pubkey) || 0) + 1)
|
|
1058
1120
|
const close = this.stopByChannel.get(pubkey)?.()
|
|
1059
|
-
if (close
|
|
1121
|
+
if (close?.then) closing.push(close)
|
|
1060
1122
|
this.stopByChannel.delete(pubkey)
|
|
1061
1123
|
this.stopPresencePublisher(pubkey)
|
|
1062
1124
|
}
|
|
@@ -1064,6 +1126,66 @@ export class PrivateMessenger {
|
|
|
1064
1126
|
return Promise.allSettled(closing)
|
|
1065
1127
|
}
|
|
1066
1128
|
|
|
1129
|
+
unwatch (channels) {
|
|
1130
|
+
const pubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.desiredChannels]
|
|
1131
|
+
for (const pubkey of pubkeys) this.desiredChannels.delete(pubkey)
|
|
1132
|
+
this.recordInterruption(pubkeys)
|
|
1133
|
+
for (const controller of this.recoveryControllers) {
|
|
1134
|
+
if (pubkeys.includes(controller.channelPubkey)) controller.abort()
|
|
1135
|
+
}
|
|
1136
|
+
return Promise.all([this.stopWatches(pubkeys), this.flushStateWrites()])
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
pause (reason) {
|
|
1140
|
+
if (typeof reason !== 'string' || !reason.trim()) throw new ValidationError('PAUSE_REASON_REQUIRED')
|
|
1141
|
+
this.assertOpen()
|
|
1142
|
+
this.pauseReasons.add(reason)
|
|
1143
|
+
this.recordInterruption(this.desiredChannels)
|
|
1144
|
+
for (const controller of this.recoveryControllers) controller.abort()
|
|
1145
|
+
return Promise.all([this.stopWatches([...this.desiredChannels]), this.flushStateWrites()])
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async resume (reason) {
|
|
1149
|
+
if (typeof reason !== 'string' || !reason.trim()) throw new ValidationError('PAUSE_REASON_REQUIRED')
|
|
1150
|
+
this.assertOpen()
|
|
1151
|
+
const removed = this.pauseReasons.delete(reason)
|
|
1152
|
+
if (reason === 'capacity' && removed) {
|
|
1153
|
+
clearInterval(this.capacityTimer)
|
|
1154
|
+
this.capacityTimer = null
|
|
1155
|
+
this.capacityRequiredBytes = 0
|
|
1156
|
+
}
|
|
1157
|
+
if (!removed || this.pauseReasons.size) return
|
|
1158
|
+
if (this.resumeWork) await this.resumeWork
|
|
1159
|
+
if (this.pauseReasons.size || this.closePromise) return
|
|
1160
|
+
const work = (async () => {
|
|
1161
|
+
const channels = [...this.desiredChannels]
|
|
1162
|
+
this.closeOpenOfflineRanges(channels)
|
|
1163
|
+
await this.watch(channels, { scheduleReloadGap: false })
|
|
1164
|
+
if (this.pauseReasons.size || this.closePromise) return
|
|
1165
|
+
await this.reconcilePresencePublishers()
|
|
1166
|
+
await this.recoverOfflineRanges(channels)
|
|
1167
|
+
})()
|
|
1168
|
+
this.resumeWork = work
|
|
1169
|
+
try { await work } catch (err) {
|
|
1170
|
+
if (!this.closePromise) await this.pause(reason)
|
|
1171
|
+
throw err
|
|
1172
|
+
} finally { if (this.resumeWork === work) this.resumeWork = null }
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
receive (pubkey, operation, message = {}) {
|
|
1176
|
+
if (this.closePromise) {
|
|
1177
|
+
if (this.storageActive) this.recordInterruption([pubkey], messageTime(message))
|
|
1178
|
+
return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
1179
|
+
}
|
|
1180
|
+
return this.queueIncoming(async () => {
|
|
1181
|
+
if (this.pauseReasons.size || !this.desiredChannels.has(pubkey)) {
|
|
1182
|
+
this.recordInterruption([pubkey], messageTime(message))
|
|
1183
|
+
throw new Error('PRIVATE_MESSENGER_PAUSED')
|
|
1184
|
+
}
|
|
1185
|
+
return operation()
|
|
1186
|
+
})
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1067
1189
|
nip65WatchChannelPubkeys () {
|
|
1068
1190
|
return [...this.channels.values()]
|
|
1069
1191
|
.filter(channel => channel.usesNip65WatchRelays && this.stopByChannel.has(channel.pubkey))
|
|
@@ -1114,6 +1236,7 @@ export class PrivateMessenger {
|
|
|
1114
1236
|
}
|
|
1115
1237
|
|
|
1116
1238
|
async handleAsk (channelPubkey, message) {
|
|
1239
|
+
if (message.provenance === 'hearsay') return this.enqueueRumor('message', channelPubkey, message)
|
|
1117
1240
|
this.trackSeederActivity(channelPubkey, message)
|
|
1118
1241
|
if (doesModeStoreRecoverySeeds(this.channels.get(channelPubkey)?.mode) && messageCode(message) === MISSING_MESSAGES_ASK_CODE) {
|
|
1119
1242
|
await this.replyWithStoredSeeds(channelPubkey, message)
|
|
@@ -1123,6 +1246,7 @@ export class PrivateMessenger {
|
|
|
1123
1246
|
}
|
|
1124
1247
|
|
|
1125
1248
|
async handleReply (channelPubkey, message) {
|
|
1249
|
+
if (message.provenance === 'hearsay') return this.enqueueRumor('message', channelPubkey, message)
|
|
1126
1250
|
this.trackSeederActivity(channelPubkey, message)
|
|
1127
1251
|
if (messageCode(message) === MISSING_MESSAGES_REPLY_CODE) {
|
|
1128
1252
|
await this.consumeMissingMessagesReply(channelPubkey, message)
|
|
@@ -1137,6 +1261,7 @@ export class PrivateMessenger {
|
|
|
1137
1261
|
}
|
|
1138
1262
|
|
|
1139
1263
|
async handleYell (channelPubkey, message) {
|
|
1264
|
+
if (message.provenance === 'hearsay') return this.enqueueRumor('message', channelPubkey, message)
|
|
1140
1265
|
this.trackSeederActivity(channelPubkey, message)
|
|
1141
1266
|
if (messageCode(message) === SEEDER_PRESENCE_CODE) return
|
|
1142
1267
|
await this.enqueueRumor('yell', channelPubkey, message)
|
|
@@ -1147,7 +1272,7 @@ export class PrivateMessenger {
|
|
|
1147
1272
|
}
|
|
1148
1273
|
|
|
1149
1274
|
async handleMessage (channelPubkey, message) {
|
|
1150
|
-
if (eventType(message.event) !== 'message') return
|
|
1275
|
+
if (eventType(message.event) !== 'message' && message.provenance !== 'hearsay' && (!message.senderPubkey || message.senderPubkey === message.event.pubkey)) return
|
|
1151
1276
|
this.trackSeederActivity(channelPubkey, message)
|
|
1152
1277
|
await this.enqueueRumor('message', channelPubkey, message)
|
|
1153
1278
|
}
|
|
@@ -1155,10 +1280,12 @@ export class PrivateMessenger {
|
|
|
1155
1280
|
async enqueueRumor (type, channelPubkey, message) {
|
|
1156
1281
|
const channel = this.channels.get(channelPubkey)
|
|
1157
1282
|
if (channel?.mode === 'watchtower' && type !== 'ask') return
|
|
1158
|
-
|
|
1283
|
+
const info = deliveryInfo(message.event, message.senderPubkey ?? message.meta?.senderPubkey ?? message.meta?.router?.tags?.find(tag => tag[0] === 'f')?.[1])
|
|
1159
1284
|
const eventId = message.event?.id || ''
|
|
1160
|
-
|
|
1285
|
+
// Stronger provenance must reach the app even while hearsay is pending.
|
|
1286
|
+
const dedupeKey = eventId ? [channelPubkey, type, eventId, info.provenance] : null
|
|
1161
1287
|
if (dedupeKey && await this.queue.someBy('byChannelTypeEventId', dedupeKey)) {
|
|
1288
|
+
this.markSeen(channelPubkey, message.outer?.created_at || message.event?.created_at || nowSeconds())
|
|
1162
1289
|
this.debug('dedupe', debugMessageInfo(type, channelPubkey, message))
|
|
1163
1290
|
return
|
|
1164
1291
|
}
|
|
@@ -1168,6 +1295,7 @@ export class PrivateMessenger {
|
|
|
1168
1295
|
channelPubkey,
|
|
1169
1296
|
receivedAt: nowSeconds(),
|
|
1170
1297
|
event: message.event,
|
|
1298
|
+
...info,
|
|
1171
1299
|
payload: message.payload,
|
|
1172
1300
|
question: message.question || null,
|
|
1173
1301
|
questionId: message.questionId || null,
|
|
@@ -1177,11 +1305,26 @@ export class PrivateMessenger {
|
|
|
1177
1305
|
} catch (err) {
|
|
1178
1306
|
// The unique index closes the small cross-instance race after `someBy`.
|
|
1179
1307
|
if (dedupeKey && err?.name === 'ConstraintError') {
|
|
1308
|
+
this.markSeen(channelPubkey, message.outer?.created_at || message.event?.created_at || nowSeconds())
|
|
1180
1309
|
this.debug('dedupe', debugMessageInfo(type, channelPubkey, message))
|
|
1181
1310
|
return
|
|
1182
1311
|
}
|
|
1312
|
+
this.recordInterruption([channelPubkey], message.outer?.created_at || message.event?.created_at || nowSeconds())
|
|
1313
|
+
// Do not await stop from inside its own transport callback.
|
|
1314
|
+
const atCapacity = err.message === 'QUEUE_CAPACITY_EXCEEDED'
|
|
1315
|
+
this.pause(atCapacity ? 'capacity' : 'storage').catch(error => this.onError?.(error))
|
|
1316
|
+
if (atCapacity) {
|
|
1317
|
+
this.capacityRequiredBytes = Math.max(this.capacityRequiredBytes, err.requiredBytes || 0)
|
|
1318
|
+
if (!this.capacityTimer) {
|
|
1319
|
+
this.capacityTimer = setInterval(() => this.checkCapacity().catch(error => this.onError?.(error)), 1000)
|
|
1320
|
+
this.capacityTimer.unref?.()
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
await this.flushStateWrites()
|
|
1183
1324
|
throw err
|
|
1184
1325
|
}
|
|
1326
|
+
this.markSeen(channelPubkey, message.outer?.created_at || message.event?.created_at || nowSeconds())
|
|
1327
|
+
this.wakeDeliveries()
|
|
1185
1328
|
this.debug('enqueue', debugMessageInfo(type, channelPubkey, message))
|
|
1186
1329
|
this.onMessageQueued?.()
|
|
1187
1330
|
}
|
|
@@ -1192,7 +1335,6 @@ export class PrivateMessenger {
|
|
|
1192
1335
|
if (seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length) {
|
|
1193
1336
|
const carriers = compactSeedNymCarriers(seed.carriers)
|
|
1194
1337
|
const recordTime = nymCarrierRecordTime({ carriers }) || seed.outer?.created_at || receivedAt
|
|
1195
|
-
this.markSeen(channelPubkey, recordTime)
|
|
1196
1338
|
const key = nymCarrierSeedKey({ channelPubkey, carriers })
|
|
1197
1339
|
const seedKey = key ? `nym:${key}` : ''
|
|
1198
1340
|
if (seedKey && await this.seedQueue.someBy('bySeedKey', seedKey)) return
|
|
@@ -1211,10 +1353,8 @@ export class PrivateMessenger {
|
|
|
1211
1353
|
}
|
|
1212
1354
|
|
|
1213
1355
|
const rows = compactSeedRouterRows(seed)
|
|
1214
|
-
let newest = seed.outer?.created_at || receivedAt
|
|
1215
1356
|
for (const row of rows) {
|
|
1216
1357
|
const rowTime = row.lastSeenAt || row.router?.created_at || receivedAt
|
|
1217
|
-
newest = Math.max(newest, rowTime)
|
|
1218
1358
|
const rowKey = routerSeedRowKey({ ...row, channelPubkey })
|
|
1219
1359
|
const seedKey = `router:${rowKey}`
|
|
1220
1360
|
const [previous] = await this.seedQueue.removeBy('bySeedKey', seedKey)
|
|
@@ -1233,20 +1373,105 @@ export class PrivateMessenger {
|
|
|
1233
1373
|
[SEED_TIME]: lastSeenAt || rowTime
|
|
1234
1374
|
})
|
|
1235
1375
|
}
|
|
1236
|
-
this.markSeen(channelPubkey, newest)
|
|
1237
1376
|
await this.pruneStoredSeeds(channelPubkey)
|
|
1238
1377
|
}
|
|
1239
1378
|
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1379
|
+
checkCapacity () {
|
|
1380
|
+
if (this.capacityCheck) return this.capacityCheck
|
|
1381
|
+
const work = (async () => {
|
|
1382
|
+
if (this.closePromise || !this.pauseReasons.has('capacity')) return
|
|
1383
|
+
const { usedBytes, maxBytes } = await this.queue.getCapacity()
|
|
1384
|
+
if (!this.closePromise && usedBytes + this.capacityRequiredBytes <= maxBytes) await this.resume('capacity')
|
|
1385
|
+
})()
|
|
1386
|
+
this.capacityCheck = work
|
|
1387
|
+
work.then(() => { this.capacityCheck = null }, () => { this.capacityCheck = null })
|
|
1388
|
+
return work
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
wakeDeliveries () {
|
|
1392
|
+
for (const wake of this.deliveryWaiters) wake()
|
|
1393
|
+
this.deliveryWaiters.clear()
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// A cancellable iterator, including while next() is waiting on an empty queue.
|
|
1397
|
+
messages () {
|
|
1398
|
+
let cancelled = false
|
|
1399
|
+
const held = new Set()
|
|
1400
|
+
let tail = Promise.resolve()
|
|
1401
|
+
const next = async () => {
|
|
1402
|
+
while (true) {
|
|
1403
|
+
if (cancelled || this.closePromise) break
|
|
1404
|
+
const delivery = await this.nextMessage()
|
|
1405
|
+
if (delivery) {
|
|
1406
|
+
if (cancelled || this.closePromise) { await delivery.nack(); break }
|
|
1407
|
+
held.add(delivery)
|
|
1408
|
+
delivery.settled.then(() => held.delete(delivery))
|
|
1409
|
+
return { value: delivery, done: false }
|
|
1410
|
+
}
|
|
1411
|
+
await new Promise(resolve => {
|
|
1412
|
+
const wake = () => { clearTimeout(timer); this.deliveryWaiters.delete(wake); resolve() }
|
|
1413
|
+
const timer = setTimeout(wake, 250)
|
|
1414
|
+
this.deliveryWaiters.add(wake)
|
|
1415
|
+
if (cancelled || this.closePromise) wake()
|
|
1416
|
+
})
|
|
1417
|
+
}
|
|
1418
|
+
return { done: true }
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
[Symbol.asyncIterator] () { return this },
|
|
1422
|
+
next: () => { const work = tail.then(next); tail = work.catch(() => {}); return work },
|
|
1423
|
+
return: async () => {
|
|
1424
|
+
cancelled = true
|
|
1425
|
+
this.wakeDeliveries()
|
|
1426
|
+
await tail
|
|
1427
|
+
await Promise.all([...held].map(delivery => delivery.nack()))
|
|
1428
|
+
return { done: true }
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1243
1431
|
}
|
|
1244
1432
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1433
|
+
nextMessage () {
|
|
1434
|
+
const work = this.reserveMessage()
|
|
1435
|
+
this.deliveryReads.add(work)
|
|
1436
|
+
work.then(() => this.deliveryReads.delete(work), () => this.deliveryReads.delete(work))
|
|
1437
|
+
return work
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async reserveMessage () {
|
|
1441
|
+
this.assertOpen()
|
|
1247
1442
|
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
1248
1443
|
await this.queueOperationTail
|
|
1249
|
-
|
|
1444
|
+
const reservation = await this.queue.reserve()
|
|
1445
|
+
if (!reservation) return null
|
|
1446
|
+
if (this.closePromise) { await reservation.nack(); return null }
|
|
1447
|
+
let finish
|
|
1448
|
+
const settled = new Promise(resolve => { finish = resolve })
|
|
1449
|
+
const done = () => {
|
|
1450
|
+
clearInterval(timer)
|
|
1451
|
+
this.deliveries.delete(delivery)
|
|
1452
|
+
finish()
|
|
1453
|
+
this.wakeDeliveries()
|
|
1454
|
+
}
|
|
1455
|
+
const settle = async method => {
|
|
1456
|
+
const result = await reservation[method]()
|
|
1457
|
+
done()
|
|
1458
|
+
if (method === 'ack' && result && !this.closePromise && this.pauseReasons.has('capacity')) {
|
|
1459
|
+
this.checkCapacity().catch(err => this.onError?.(err))
|
|
1460
|
+
}
|
|
1461
|
+
return result
|
|
1462
|
+
}
|
|
1463
|
+
const delivery = {
|
|
1464
|
+
message: withoutQueueMetadata(reservation.item),
|
|
1465
|
+
ack: () => settle('ack'),
|
|
1466
|
+
nack: () => settle('nack')
|
|
1467
|
+
}
|
|
1468
|
+
Object.defineProperty(delivery, 'settled', { value: settled })
|
|
1469
|
+
this.deliveries.add(delivery)
|
|
1470
|
+
const timer = setInterval(() => {
|
|
1471
|
+
reservation.renew().then(ok => { if (!ok) done() }, err => { done(); this.onError?.(err) })
|
|
1472
|
+
}, 10000)
|
|
1473
|
+
timer.unref?.()
|
|
1474
|
+
return delivery
|
|
1250
1475
|
}
|
|
1251
1476
|
|
|
1252
1477
|
async ask ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkey, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
@@ -1444,13 +1669,14 @@ export class PrivateMessenger {
|
|
|
1444
1669
|
}
|
|
1445
1670
|
|
|
1446
1671
|
async startPresencePublisher (channelPubkey) {
|
|
1447
|
-
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
1672
|
+
if (this.pauseReasons.size || !this.desiredChannels.has(channelPubkey) || !this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
1448
1673
|
if (this.presenceTimers.has(channelPubkey)) return
|
|
1449
1674
|
try {
|
|
1450
1675
|
await this.publishSeederPresence(channelPubkey)
|
|
1451
1676
|
} catch (err) {
|
|
1452
1677
|
console.warn('private-messenger seeder presence failed', err?.message ?? err)
|
|
1453
1678
|
}
|
|
1679
|
+
if (this.pauseReasons.size || this.closePromise || !this.desiredChannels.has(channelPubkey)) return
|
|
1454
1680
|
const timer = this._setInterval(() => {
|
|
1455
1681
|
return this.publishSeederPresence(channelPubkey).catch(err => {
|
|
1456
1682
|
console.warn('private-messenger seeder presence failed', err?.message ?? err)
|
|
@@ -1469,10 +1695,10 @@ export class PrivateMessenger {
|
|
|
1469
1695
|
async reconcilePresencePublishers () {
|
|
1470
1696
|
const starts = []
|
|
1471
1697
|
for (const pubkey of [...this.presenceTimers.keys()]) {
|
|
1472
|
-
if (!doesModeStoreRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1698
|
+
if (this.pauseReasons.size || !this.desiredChannels.has(pubkey) || !doesModeStoreRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1473
1699
|
}
|
|
1474
1700
|
for (const [pubkey, channel] of this.channels) {
|
|
1475
|
-
if (doesModeStoreRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1701
|
+
if (!this.pauseReasons.size && this.desiredChannels.has(pubkey) && doesModeStoreRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1476
1702
|
else this.stopPresencePublisher(pubkey)
|
|
1477
1703
|
}
|
|
1478
1704
|
await Promise.all(starts)
|
|
@@ -1497,6 +1723,8 @@ export class PrivateMessenger {
|
|
|
1497
1723
|
}
|
|
1498
1724
|
|
|
1499
1725
|
requireWritableChannel (pubkey) {
|
|
1726
|
+
this.assertOpen()
|
|
1727
|
+
if (this.pauseReasons.size) throw new Error('PRIVATE_MESSENGER_PAUSED')
|
|
1500
1728
|
const channel = this.requireChannel(pubkey)
|
|
1501
1729
|
if (!channel.signer) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
1502
1730
|
return channel
|
|
@@ -1576,12 +1804,22 @@ export class PrivateMessenger {
|
|
|
1576
1804
|
return this.useContentKeys ? undefined : noContentKeys
|
|
1577
1805
|
}
|
|
1578
1806
|
|
|
1807
|
+
recordRecoveryWindow (pubkey) {
|
|
1808
|
+
const seconds = this.offlineRecoverySecondsFor(pubkey)
|
|
1809
|
+
if (!seconds) return
|
|
1810
|
+
const now = nowSeconds()
|
|
1811
|
+
const current = this.readState().channels[pubkey] || {}
|
|
1812
|
+
const checkpoint = Math.max(current.lastSeenAt || 0, current.recoveredThrough || 0)
|
|
1813
|
+
const since = checkpoint ? checkpoint - this.offlineSkewSeconds : now - seconds
|
|
1814
|
+
this.addOfflineRange(pubkey, Math.max(0, now - seconds, since), now)
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1579
1817
|
scheduleReloadGap (pubkey) {
|
|
1580
1818
|
this.cancelReloadGap(pubkey)
|
|
1581
1819
|
if (!this.offlineRecoverySecondsFor(pubkey)) return
|
|
1582
1820
|
const current = this.readState().channels[pubkey]
|
|
1583
|
-
const start = current?.openOfflineStart
|
|
1584
|
-
if (!
|
|
1821
|
+
const start = current?.openOfflineStart ?? current?.lastSeenAt
|
|
1822
|
+
if ((start == null && !current?.offlineRanges?.length) || this.pauseReasons.size) return
|
|
1585
1823
|
const revision = this.watchRevisionByChannel.get(pubkey) || 0
|
|
1586
1824
|
const token = {}
|
|
1587
1825
|
const timer = this._setTimeout(async () => {
|
|
@@ -1590,8 +1828,8 @@ export class PrivateMessenger {
|
|
|
1590
1828
|
this.reloadGapTimers.delete(pubkey)
|
|
1591
1829
|
if (this.closePromise || !this.channels.has(pubkey) || !this.stopByChannel.has(pubkey)) return
|
|
1592
1830
|
if ((this.watchRevisionByChannel.get(pubkey) || 0) !== revision) return
|
|
1593
|
-
this.addOfflineRange(pubkey, Math.max(0, start - this.offlineSkewSeconds), nowSeconds())
|
|
1594
|
-
await this.recoverOfflineRanges([pubkey])
|
|
1831
|
+
if (start != null) this.addOfflineRange(pubkey, Math.max(0, start - this.offlineSkewSeconds), nowSeconds())
|
|
1832
|
+
try { await this.recoverOfflineRanges([pubkey]) } catch (err) { this.onError?.(err) }
|
|
1595
1833
|
}, this.reloadGapDelayMs)
|
|
1596
1834
|
this.reloadGapTimers.set(pubkey, { timer, token, revision })
|
|
1597
1835
|
}
|
|
@@ -1603,47 +1841,15 @@ export class PrivateMessenger {
|
|
|
1603
1841
|
this._clearTimeout(scheduled.timer)
|
|
1604
1842
|
}
|
|
1605
1843
|
|
|
1606
|
-
// Browser-offline recovery owns durable gaps. Stop only the child live reads;
|
|
1607
|
-
// unwatch() would also stop seeder-presence publishing and alter channel state.
|
|
1608
|
-
#pauseLiveWatches () {
|
|
1609
|
-
for (const [pubkey, stop] of this.stopByChannel) {
|
|
1610
|
-
this.cancelReloadGap(pubkey)
|
|
1611
|
-
this.watchRevisionByChannel.set(pubkey, (this.watchRevisionByChannel.get(pubkey) || 0) + 1)
|
|
1612
|
-
stop?.()
|
|
1613
|
-
}
|
|
1614
|
-
this.stopByChannel.clear()
|
|
1615
|
-
}
|
|
1616
|
-
|
|
1617
|
-
async #resumeLiveWatches () {
|
|
1618
|
-
const channelPubkeys = [...this.channels.keys()]
|
|
1619
|
-
await this.watch(channelPubkeys, { scheduleReloadGap: false })
|
|
1620
|
-
return channelPubkeys
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
1844
|
ensureNetworkWatchers () {
|
|
1624
1845
|
if (typeof window === 'undefined') return
|
|
1625
1846
|
if (!this.stopOffline) {
|
|
1626
|
-
const offline = () => {
|
|
1627
|
-
const state = this.readState()
|
|
1628
|
-
const start = Math.max(0, nowSeconds() - this.offlineSkewSeconds)
|
|
1629
|
-
for (const pubkey of this.channels.keys()) {
|
|
1630
|
-
if (!this.offlineRecoverySecondsFor(pubkey)) continue
|
|
1631
|
-
const current = state.channels[pubkey] || {}
|
|
1632
|
-
current.openOfflineStart ||= start
|
|
1633
|
-
state.channels[pubkey] = current
|
|
1634
|
-
}
|
|
1635
|
-
this.writeState(state)
|
|
1636
|
-
this.#pauseLiveWatches()
|
|
1637
|
-
}
|
|
1847
|
+
const offline = () => { this.pause('network').catch(err => this.onError?.(err)) }
|
|
1638
1848
|
window.addEventListener('offline', offline)
|
|
1639
1849
|
this.stopOffline = () => window.removeEventListener('offline', offline)
|
|
1640
1850
|
}
|
|
1641
1851
|
if (!this.stopOnline) {
|
|
1642
|
-
const online =
|
|
1643
|
-
this.closeOpenOfflineRanges()
|
|
1644
|
-
const channelPubkeys = await this.#resumeLiveWatches()
|
|
1645
|
-
await this.recoverOfflineRanges(channelPubkeys)
|
|
1646
|
-
}
|
|
1852
|
+
const online = () => { this.resume('network').catch(err => this.onError?.(err)) }
|
|
1647
1853
|
window.addEventListener('online', online)
|
|
1648
1854
|
this.stopOnline = () => window.removeEventListener('online', online)
|
|
1649
1855
|
}
|
|
@@ -1729,6 +1935,7 @@ export class PrivateMessenger {
|
|
|
1729
1935
|
await this.enqueueRumor(recovered.type, channelPubkey, {
|
|
1730
1936
|
event: recovered.event,
|
|
1731
1937
|
outer: recovered.outer,
|
|
1938
|
+
...deliveryInfo(recovered.event, recovered.meta?.senderPubkey),
|
|
1732
1939
|
meta: { ...(recovered.meta || {}), channelPubkey, recoveredFromSeeder: message.event?.pubkey || '' },
|
|
1733
1940
|
payload: recovered.payload
|
|
1734
1941
|
})
|
|
@@ -1742,7 +1949,7 @@ export class PrivateMessenger {
|
|
|
1742
1949
|
type: 'nym',
|
|
1743
1950
|
event,
|
|
1744
1951
|
outer: { id: '', created_at: nymCarrierRecordTime(record) },
|
|
1745
|
-
meta: { channelPubkey, carriers: record.carriers },
|
|
1952
|
+
meta: { channelPubkey, carriers: record.carriers, ...deliveryInfo(event, record.carriers[0]?.pubkey) },
|
|
1746
1953
|
payload: parseEventContent(event)
|
|
1747
1954
|
}
|
|
1748
1955
|
}
|
|
@@ -1787,12 +1994,24 @@ export class PrivateMessenger {
|
|
|
1787
1994
|
type: eventType(event),
|
|
1788
1995
|
event,
|
|
1789
1996
|
outer,
|
|
1790
|
-
meta: { channelPubkey },
|
|
1997
|
+
meta: { channelPubkey, ...deliveryInfo(event, router.tags.find(tag => tag[0] === 'f')?.[1]) },
|
|
1791
1998
|
payload: parseEventContent(event)
|
|
1792
1999
|
}
|
|
1793
2000
|
}
|
|
1794
2001
|
|
|
1795
2002
|
async recoverOfflineRanges (channels = [...this.stopByChannel.keys()]) {
|
|
2003
|
+
for (const pubkey of uniq(channels)) {
|
|
2004
|
+
if (this.pauseReasons.size || this.closePromise || !this.desiredChannels.has(pubkey)) continue
|
|
2005
|
+
let work = this.recoveries.get(pubkey)
|
|
2006
|
+
if (!work) {
|
|
2007
|
+
work = this.recoverChannelRanges([pubkey])
|
|
2008
|
+
this.recoveries.set(pubkey, work)
|
|
2009
|
+
}
|
|
2010
|
+
try { await work } finally { if (this.recoveries.get(pubkey) === work) this.recoveries.delete(pubkey) }
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
async recoverChannelRanges (channels) {
|
|
1796
2015
|
const state = this.readState()
|
|
1797
2016
|
const now = nowSeconds()
|
|
1798
2017
|
|
|
@@ -1806,12 +2025,20 @@ export class PrivateMessenger {
|
|
|
1806
2025
|
const processedRanges = new Set(current.offlineRanges.map(range => `${range.start}:${range.end}`))
|
|
1807
2026
|
|
|
1808
2027
|
const remaining = []
|
|
2028
|
+
let recoveredThrough = current.recoveredThrough || 0
|
|
1809
2029
|
for (const range of current.offlineRanges) {
|
|
1810
2030
|
if (range.end < minStart) continue
|
|
1811
2031
|
const watchRevision = this.watchRevisionByChannel.get(pubkey) || 0
|
|
2032
|
+
const controller = new AbortController()
|
|
2033
|
+
controller.channelPubkey = pubkey
|
|
2034
|
+
this.recoveryControllers.add(controller)
|
|
1812
2035
|
try {
|
|
2036
|
+
if (this.pauseReasons.size || this.closePromise || !this.desiredChannels.has(pubkey)) throw new Error('PRIVATE_MESSENGER_PAUSED')
|
|
1813
2037
|
const fetchRelays = await this.resolveWatchRelays(channel)
|
|
2038
|
+
controller.signal.throwIfAborted()
|
|
1814
2039
|
const fetchedEvents = await this._privateChannel.fetch({
|
|
2040
|
+
signal: controller.signal,
|
|
2041
|
+
receivedChunkScope: this.storageLeaseId,
|
|
1815
2042
|
receiverSigner: this.userSigner,
|
|
1816
2043
|
iykcSigner: this.contentKeySigner,
|
|
1817
2044
|
privateChannelSigner: channel.signer,
|
|
@@ -1826,36 +2053,40 @@ export class PrivateMessenger {
|
|
|
1826
2053
|
modeByPubkey: { [pubkey]: channel.mode },
|
|
1827
2054
|
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1828
2055
|
receivedChunkIndexedDB: this._indexedDB,
|
|
1829
|
-
onEvent: (event, outer, meta) => this.
|
|
1830
|
-
onNymEvent: (event, outer, meta) => this.
|
|
1831
|
-
onSeedEvent: seed => this.
|
|
2056
|
+
onEvent: (event, outer, meta) => this.receive(pubkey, () => this.enqueueRumor(eventType(event), pubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }),
|
|
2057
|
+
onNymEvent: (event, outer, meta) => this.receive(pubkey, () => this.enqueueRumor('nym', pubkey, { event, outer, meta, payload: parseEventContent(event) }), { event, outer }),
|
|
2058
|
+
onSeedEvent: seed => this.receive(pubkey, () => this.enqueueSeed(pubkey, seed), seed),
|
|
1832
2059
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
1833
2060
|
onError: err => { throw err }
|
|
1834
2061
|
}) || []
|
|
2062
|
+
controller.signal.throwIfAborted()
|
|
1835
2063
|
const attempt = await this.#askSeedersForRelayLeftEdgeAttempt(pubkey, range, fetchedEvents)
|
|
1836
2064
|
const lifecycleChanged = this.closePromise || !this.channels.has(pubkey) || !this.stopByChannel.has(pubkey) ||
|
|
1837
2065
|
(this.watchRevisionByChannel.get(pubkey) || 0) !== watchRevision
|
|
1838
2066
|
if (lifecycleChanged || attempt.failures.length) remaining.push(range)
|
|
2067
|
+
else recoveredThrough = Math.max(recoveredThrough, range.end)
|
|
1839
2068
|
} catch (err) {
|
|
1840
2069
|
this.onError?.(err)
|
|
1841
2070
|
remaining.push(range)
|
|
1842
|
-
}
|
|
2071
|
+
} finally { this.recoveryControllers.delete(controller) }
|
|
1843
2072
|
}
|
|
1844
2073
|
const fresh = this.readState()
|
|
1845
2074
|
const concurrentRanges = (fresh.channels[pubkey]?.offlineRanges || [])
|
|
1846
2075
|
.filter(range => !processedRanges.has(`${range.start}:${range.end}`))
|
|
1847
2076
|
fresh.channels[pubkey] = {
|
|
1848
2077
|
...(fresh.channels[pubkey] || {}),
|
|
2078
|
+
recoveredThrough: Math.max(fresh.channels[pubkey]?.recoveredThrough || 0, recoveredThrough),
|
|
1849
2079
|
offlineRanges: mergeRanges(concurrentRanges.concat(remaining))
|
|
1850
2080
|
}
|
|
1851
2081
|
this.writeState(fresh)
|
|
2082
|
+
await this.flushStateWrites()
|
|
1852
2083
|
}
|
|
1853
2084
|
}
|
|
1854
2085
|
|
|
1855
2086
|
async clearChannel (pubkey) {
|
|
2087
|
+
await this.unwatch(pubkey)
|
|
2088
|
+
await this._privateMessage.clearChannelState?.(pubkey)
|
|
1856
2089
|
return this.runQueueOperation(async () => {
|
|
1857
|
-
await this.unwatch(pubkey)
|
|
1858
|
-
await this._privateMessage.clearChannelState?.(pubkey)
|
|
1859
2090
|
this.channels.delete(pubkey)
|
|
1860
2091
|
this.removeChannelState(pubkey)
|
|
1861
2092
|
await this.flushStateWrites()
|
|
@@ -1938,6 +2169,9 @@ export class PrivateMessenger {
|
|
|
1938
2169
|
this.stopOnline = null
|
|
1939
2170
|
this.stopStorageMaintenance()
|
|
1940
2171
|
this.stopStoragePolicyBroadcast()
|
|
2172
|
+
clearInterval(this.capacityTimer)
|
|
2173
|
+
this.capacityTimer = null
|
|
2174
|
+
this.wakeDeliveries()
|
|
1941
2175
|
|
|
1942
2176
|
this.closePromise = (async () => {
|
|
1943
2177
|
let unwatchError
|
|
@@ -1945,6 +2179,9 @@ export class PrivateMessenger {
|
|
|
1945
2179
|
await initSettledPromise
|
|
1946
2180
|
await this.stampActiveChannelActivity()
|
|
1947
2181
|
await this.queueOperationTail
|
|
2182
|
+
await Promise.allSettled([...this.recoveries.values()])
|
|
2183
|
+
await Promise.allSettled([...this.deliveryReads])
|
|
2184
|
+
await Promise.all([...this.deliveries].map(delivery => delivery.nack()))
|
|
1948
2185
|
await this.stateWriteTail
|
|
1949
2186
|
try { await this.storageTouchPromise } catch {}
|
|
1950
2187
|
await this.storageMaintenancePromise
|