libp2r2p 0.12.0 → 0.12.2
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 +51 -2
- package/helpers/secp256k1-wasm/backend.js +2 -3
- package/package.json +3 -3
- package/private-channel/helpers/prepared.js +12 -0
- package/private-channel/index.js +271 -32
- package/private-message/index.js +44 -23
- package/private-messenger/file/index.js +4 -2
- package/private-messenger/helpers/fallback-race.js +13 -2
- package/private-messenger/helpers/send-routing.js +2 -1
- package/private-messenger/index.js +30 -15
- package/private-messenger/services/pending-decryption.js +20 -4
- package/private-messenger/session/README.md +17 -3
- package/private-messenger/session/helpers/prepared-state.js +17 -0
- package/private-messenger/session/helpers/publication-journal.js +38 -10
- package/private-messenger/session/index.js +21 -9
- package/relay/services/relay-pool.js +127 -58
package/private-message/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ValidationError } from '../error/index.js'
|
|
|
4
4
|
import { generateKeypair, keypairFromSeckey } from '../key/index.js'
|
|
5
5
|
import * as privateChannel from '../private-channel/index.js'
|
|
6
6
|
import { normalizeRumor, deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
7
|
+
import { normalizePreparedPublications } from '../private-channel/helpers/prepared.js'
|
|
7
8
|
|
|
8
9
|
export const ASK_KIND = 7329
|
|
9
10
|
export const REPLY_KIND = 7330
|
|
@@ -41,17 +42,25 @@ function normalizeDeletionPubkey (deletionPubkey) {
|
|
|
41
42
|
return deletionPubkey.toLowerCase()
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeletionCapability = true,
|
|
45
|
+
function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeletionCapability = true, preparedPublications = [] }) {
|
|
45
46
|
if (deletionSeckey !== undefined) throw new ValidationError('DELETION_SECKEY_NOT_ACCEPTED')
|
|
46
47
|
if (typeof autoDeletionCapability !== 'boolean') throw new ValidationError('AUTO_DELETION_CAPABILITY_BOOLEAN_REQUIRED')
|
|
47
48
|
|
|
48
49
|
const normalizedDeletionPubkey = normalizeDeletionPubkey(deletionPubkey)
|
|
49
|
-
if (
|
|
50
|
-
const capability =
|
|
51
|
-
if (!capability
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (preparedPublications.length) {
|
|
51
|
+
const capability = preparedPublications[0].deletionCapability
|
|
52
|
+
if (!capability) throw new ValidationError('INVALID_PREPARED_PUBLICATION')
|
|
53
|
+
for (const publication of preparedPublications) {
|
|
54
|
+
const current = publication.deletionCapability
|
|
55
|
+
if (!current || (current.deletionPubkey !== capability.deletionPubkey) ||
|
|
56
|
+
(current.deletionSeckey !== capability.deletionSeckey) ||
|
|
57
|
+
(current.deletionPubkey !== undefined && normalizeDeletionPubkey(current.deletionPubkey) !== current.deletionPubkey) ||
|
|
58
|
+
(normalizedDeletionPubkey && normalizedDeletionPubkey !== capability.deletionPubkey) ||
|
|
59
|
+
(capability.deletionSeckey !== undefined && keypairFromSeckey(capability.deletionSeckey).pubkey !== capability.deletionPubkey) ||
|
|
60
|
+
publication.events?.some(event => event.tags.find(tag => tag[0] === 's')?.[1] !== capability.deletionPubkey)) {
|
|
61
|
+
throw new ValidationError('INVALID_PREPARED_PUBLICATION')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
55
64
|
return { ...capability }
|
|
56
65
|
}
|
|
57
66
|
if (normalizedDeletionPubkey) return { deletionPubkey: normalizedDeletionPubkey }
|
|
@@ -63,9 +72,9 @@ function resolveDeletionCapability ({ deletionPubkey, deletionSeckey, autoDeleti
|
|
|
63
72
|
|
|
64
73
|
function preparedSeedHandler (onPreparedSeed, deletion) {
|
|
65
74
|
if (!onPreparedSeed) return undefined
|
|
66
|
-
return seed => {
|
|
75
|
+
return (seed, options) => {
|
|
67
76
|
seed.publication.deletionCapability = deletion
|
|
68
|
-
return onPreparedSeed(seed)
|
|
77
|
+
return onPreparedSeed(seed, options)
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -668,11 +677,11 @@ async function sendPrivateMessage ({
|
|
|
668
677
|
_getIykcProofs,
|
|
669
678
|
fileChunkIndex,
|
|
670
679
|
onPreparedSeed,
|
|
671
|
-
|
|
680
|
+
preparedPublications,
|
|
672
681
|
_publish = privateChannel.publish
|
|
673
682
|
}) {
|
|
674
683
|
if (!privateChannelSigner?.getPublicKey) throw new ValidationError('PRIVATE_CHANNEL_WRITER_REQUIRED')
|
|
675
|
-
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed,
|
|
684
|
+
return _publish({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag, deletionPubkey, event, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, preparedPublications: preparedPublications?.length ? preparedPublications : undefined, _getIykcProofs })
|
|
676
685
|
}
|
|
677
686
|
|
|
678
687
|
async function sendNymMessage ({
|
|
@@ -841,21 +850,29 @@ export async function yell ({
|
|
|
841
850
|
deletionSeckey,
|
|
842
851
|
autoDeletionCapability = true,
|
|
843
852
|
fileChunkIndex,
|
|
853
|
+
createdAt,
|
|
844
854
|
onPreparedSeed,
|
|
855
|
+
preparedPublication,
|
|
856
|
+
preparedPublications,
|
|
845
857
|
_publish = privateChannel.publish
|
|
846
858
|
}) {
|
|
847
|
-
const
|
|
859
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
860
|
+
const receivers = uniq(receiverPubkeys?.length
|
|
861
|
+
? receiverPubkeys
|
|
862
|
+
: prepared.flatMap(publication => publication.receiverPubkeys || []))
|
|
848
863
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
864
|
+
const rumor = makeMessageRumor({
|
|
865
|
+
kind: TELL_KIND,
|
|
866
|
+
tags: [],
|
|
867
|
+
message: message || { code, payload, error, content }
|
|
868
|
+
})
|
|
869
|
+
if (createdAt !== undefined) rumor.created_at = createdAt
|
|
849
870
|
const { event, wireEvent } = await makeOutgoingRumor({
|
|
850
871
|
senderSigner,
|
|
851
|
-
rumor
|
|
852
|
-
kind: TELL_KIND,
|
|
853
|
-
tags: [],
|
|
854
|
-
message: message || { code, payload, error, content }
|
|
855
|
-
})
|
|
872
|
+
rumor
|
|
856
873
|
})
|
|
857
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability })
|
|
858
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed, _getIykcProofs, _publish })
|
|
874
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
875
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
859
876
|
return withDelivery({ yell: event }, reports, deletion.deletionSeckey)
|
|
860
877
|
}
|
|
861
878
|
|
|
@@ -878,13 +895,15 @@ export async function broadcastRumor ({
|
|
|
878
895
|
fileChunkIndex,
|
|
879
896
|
onPreparedSeed,
|
|
880
897
|
preparedPublication,
|
|
898
|
+
preparedPublications,
|
|
881
899
|
_publish = privateChannel.publish
|
|
882
900
|
}) {
|
|
883
901
|
const receivers = uniq(receiverPubkeys)
|
|
884
902
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
903
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
885
904
|
const { event, wireEvent } = await makeOutgoingRumor({ senderSigner, rumor })
|
|
886
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability,
|
|
887
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion),
|
|
905
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
906
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
888
907
|
return withDelivery({ rumor: event }, reports, deletion.deletionSeckey)
|
|
889
908
|
}
|
|
890
909
|
|
|
@@ -907,13 +926,15 @@ export async function broadcastEvent ({
|
|
|
907
926
|
fileChunkIndex,
|
|
908
927
|
onPreparedSeed,
|
|
909
928
|
preparedPublication,
|
|
929
|
+
preparedPublications,
|
|
910
930
|
_publish = privateChannel.publish
|
|
911
931
|
}) {
|
|
912
932
|
const receivers = uniq(receiverPubkeys)
|
|
913
933
|
if (!receivers.length) throw new ValidationError('NO_RECEIVERS')
|
|
934
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
914
935
|
const wireEvent = assertValidSignedEvent({ ...event, tags: cloneTags(event?.tags) })
|
|
915
|
-
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability,
|
|
916
|
-
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion),
|
|
936
|
+
const deletion = resolveDeletionCapability({ deletionPubkey, deletionSeckey, autoDeletionCapability, preparedPublications: prepared })
|
|
937
|
+
const reports = await sendPrivateMessage({ senderSigner, imkcSigner, privateChannelSigner, privateChannelReaderPubkey, receivers, receiverTag: '', deletionPubkey: deletion.deletionPubkey, event: wireEvent, relays, relayToReceivers, recoveryRelays, expirationSeconds, temporaryStorageArea, fileChunkIndex, onPreparedSeed: preparedSeedHandler(onPreparedSeed, deletion), preparedPublications: prepared, _getIykcProofs, _publish })
|
|
917
938
|
return withDelivery({ event: wireEvent }, reports, deletion.deletionSeckey)
|
|
918
939
|
}
|
|
919
940
|
|
|
@@ -4,6 +4,7 @@ import { createFileCache } from './helpers/cache.js'
|
|
|
4
4
|
export { createFileCache, DEFAULT_FILE_CACHE_BYTES } from './helpers/cache.js'
|
|
5
5
|
import { createFileCatalog } from './helpers/catalog.js'
|
|
6
6
|
import { assertMessagePublished } from '../helpers/publication.js'
|
|
7
|
+
import { normalizePreparedPublications } from '../../private-channel/helpers/prepared.js'
|
|
7
8
|
import { ValidationError } from '../../error/index.js'
|
|
8
9
|
import { IRFS_CHUNK_BYTES, decodeIrfsChunk } from '../../irfs/index.js'
|
|
9
10
|
import { createQueue } from '../../idb-queue/index.js'
|
|
@@ -151,9 +152,10 @@ export function createPrivateFileTransfer ({ messenger, resolveChannel, storage,
|
|
|
151
152
|
function sendOptions (file, routes) {
|
|
152
153
|
return { senderSigner: messenger.userSigner, imkcSigner: messenger.contentKeySigner, privateChannelSigner: file.signer, ...routes, temporaryStorageArea: messenger.temporaryStorageArea, expirationSeconds: messenger.eventExpirationSecondsFor(file.controlChannelPubkey), _getIykcProofs: messenger.contentKeyLookup() }
|
|
153
154
|
}
|
|
154
|
-
async function publishChunk (descriptor, event, { signal, attemptStartedAt, onPreparedSeed, preparedPublication, onDebug } = {}) {
|
|
155
|
+
async function publishChunk (descriptor, event, { signal, attemptStartedAt, onPreparedSeed, preparedPublication, preparedPublications, onDebug } = {}) {
|
|
155
156
|
const file = await register(descriptor)
|
|
156
157
|
usable(file)
|
|
158
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
157
159
|
const decoded = checked(file, event)
|
|
158
160
|
await catalog.ready
|
|
159
161
|
if (ownsStorage) {
|
|
@@ -162,7 +164,7 @@ export function createPrivateFileTransfer ({ messenger, resolveChannel, storage,
|
|
|
162
164
|
}
|
|
163
165
|
let result
|
|
164
166
|
try {
|
|
165
|
-
const options = { ...sendOptions(file, await routing(file, file.peerPubkey, signal, attemptStartedAt, onDebug)), receiverPubkeys: [file.peerPubkey], fileChunkIndex: decoded.index,
|
|
167
|
+
const options = { ...sendOptions(file, await routing(file, file.peerPubkey, signal, attemptStartedAt, onDebug)), receiverPubkeys: [file.peerPubkey], fileChunkIndex: decoded.index, preparedPublications: prepared.length ? prepared : undefined, ...(onPreparedSeed ? { onPreparedSeed } : messenger.channels.get(file.controlChannelPubkey)?.mode === 'watchtower' ? { onPreparedSeed: seed => saveSeed(file, seed) } : {}) }
|
|
166
168
|
result = event.sig ? await _messages.broadcastEvent({ ...options, event }) : await _messages.broadcastRumor({ ...options, rumor: event })
|
|
167
169
|
await assertMessagePublished(result, result?.rumor || result?.event || { ...event, pubkey: event.pubkey || messenger.userPubkey })
|
|
168
170
|
} catch (error) { await uploadLeases.get(file.root)?.(); uploadLeases.delete(file.root); throw error }
|
|
@@ -2,7 +2,7 @@ import { isReplaceableRelayFailure } from '../../relay/helpers/failure.js'
|
|
|
2
2
|
|
|
3
3
|
// Each invocation owns one already signed outer event and its encrypted subset.
|
|
4
4
|
// Lanes share coverage and the tried set; only started reports outlive the race.
|
|
5
|
-
export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tried, nextPrimary, nextFallback, sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess = () => {}, delay, attemptStartedAt, monotonicNow = () => performance.now(), now, setTimer, clearTimer }) {
|
|
5
|
+
export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tried, getRetryEvent, nextPrimary, nextFallback, sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess = () => {}, delay, attemptStartedAt, monotonicNow = () => performance.now(), now, setTimer, clearTimer }) {
|
|
6
6
|
const pending = new Set(receivers)
|
|
7
7
|
const summaries = []
|
|
8
8
|
const transportSignal = signal && pauseSignal ? AbortSignal.any([signal, pauseSignal]) : signal || pauseSignal
|
|
@@ -33,6 +33,13 @@ export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tr
|
|
|
33
33
|
...(interruptedWork && !signal?.aborted ? { retryWhenAvailable: true, ...(offline ? { retryWhenOnline: true } : {}) } : {})
|
|
34
34
|
})
|
|
35
35
|
}
|
|
36
|
+
const fail = error => {
|
|
37
|
+
if (done) return
|
|
38
|
+
done = true
|
|
39
|
+
clearTimer(timer); timer = null
|
|
40
|
+
transportSignal?.removeEventListener('abort', interrupted)
|
|
41
|
+
outcome.reject(error)
|
|
42
|
+
}
|
|
36
43
|
const check = () => {
|
|
37
44
|
if (done) return
|
|
38
45
|
if (!pending.size) finish(true)
|
|
@@ -59,7 +66,11 @@ export function publishWithEarlyFallback ({ event, receivers, first, mirrors, tr
|
|
|
59
66
|
const slot = Promise.withResolvers()
|
|
60
67
|
summaries.push(slot.promise)
|
|
61
68
|
let result
|
|
62
|
-
try { result = await sendEvent(event, relays, { signal: transportSignal }) } catch (reason) {
|
|
69
|
+
try { result = await sendEvent(event, relays, { signal: transportSignal, getRetryEvent }) } catch (reason) {
|
|
70
|
+
if (reason?.code === 'RETRY_EVENT_PREPARATION_FAILED') {
|
|
71
|
+
fail(reason)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
63
74
|
result = { success: false, promise: Promise.resolve({ fulfilled: 0, errors: [{ reason }] }) }
|
|
64
75
|
}
|
|
65
76
|
const settled = Promise.resolve(result.promise).catch(reason => ({ fulfilled: 0, errors: [{ reason }] }))
|
|
@@ -123,6 +123,7 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
123
123
|
}
|
|
124
124
|
const result = await publishWithEarlyFallback({
|
|
125
125
|
event, receivers, first: first.length ? { receivers, relays: first } : null, mirrors, tried,
|
|
126
|
+
getRetryEvent: context.getRetryEvent,
|
|
126
127
|
nextPrimary: pending => pick(pending, false), nextFallback: pending => pick(pending, true),
|
|
127
128
|
sendEvent, isOnline, isCurrent, signal, pauseSignal, remember, rememberSuccess,
|
|
128
129
|
delay: fallbackDelayMs, attemptStartedAt, monotonicNow, now, setTimer, clearTimer
|
|
@@ -158,7 +159,7 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
158
159
|
for (const relay of relays) tried.add(normalized(relay))
|
|
159
160
|
// Every relay in this batch serves the same pending recipient subset.
|
|
160
161
|
// One ACK covers that subset, never unrelated members of the ciphertext.
|
|
161
|
-
result = await sendEvent(event, relays, { signal: transportSignal })
|
|
162
|
+
result = await sendEvent(event, relays, { signal: transportSignal, getRetryEvent: context.getRetryEvent })
|
|
162
163
|
const settled = Promise.resolve(result.promise)
|
|
163
164
|
summaries.push(settled)
|
|
164
165
|
settled.then(report => rememberSuccess(report?.succeededRelays || [])).catch(() => {})
|
|
@@ -56,6 +56,7 @@ import { recoveryRetryDelay, isPermanentRecoveryError } from './helpers/recovery
|
|
|
56
56
|
import { assertMessagePublished, publicationFailureDiagnostic, sanitizePublicationError } from './helpers/publication.js'
|
|
57
57
|
import { normalizeRelayUrl } from '../url/index.js'
|
|
58
58
|
import { deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
59
|
+
import { normalizePreparedPublications } from '../private-channel/helpers/prepared.js'
|
|
59
60
|
import { migrateMessageQueue } from './services/migrate-message-queue.js'
|
|
60
61
|
import { createPendingDecryption, pendingContentKeyGroup, pendingMissingContentKeyGroup } from './services/pending-decryption.js'
|
|
61
62
|
import { createDecryptionScheduler } from './services/decryption-scheduler.js'
|
|
@@ -2415,18 +2416,22 @@ export class PrivateMessenger {
|
|
|
2415
2416
|
} finally { releaseActivity() }
|
|
2416
2417
|
}
|
|
2417
2418
|
|
|
2418
|
-
async yell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey }) {
|
|
2419
|
+
async yell ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, message, code, payload, error, content, deletionPubkey, createdAt, onPreparedSeed, preparedPublication, preparedPublications }) {
|
|
2419
2420
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2420
2421
|
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2421
2422
|
try {
|
|
2422
|
-
const
|
|
2423
|
-
|
|
2423
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
2424
|
+
const receivers = uniq(receiverPubkeys?.length
|
|
2425
|
+
? receiverPubkeys
|
|
2426
|
+
: prepared.flatMap(publication => publication.receiverPubkeys || []))
|
|
2427
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: receivers, relays, relayToReceivers })
|
|
2428
|
+
this.debugSend('yell', channelPubkey, { code, receiverPubkeys: receivers })
|
|
2424
2429
|
return await this._privateMessage.yell({
|
|
2425
2430
|
senderSigner: this.userSigner,
|
|
2426
2431
|
imkcSigner: this.contentKeySigner,
|
|
2427
2432
|
privateChannelSigner: channel.signer,
|
|
2428
2433
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2429
|
-
receiverPubkeys,
|
|
2434
|
+
receiverPubkeys: receivers,
|
|
2430
2435
|
...routing,
|
|
2431
2436
|
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2432
2437
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
@@ -2437,24 +2442,30 @@ export class PrivateMessenger {
|
|
|
2437
2442
|
payload,
|
|
2438
2443
|
error,
|
|
2439
2444
|
content,
|
|
2440
|
-
|
|
2445
|
+
createdAt,
|
|
2446
|
+
onPreparedSeed: onPreparedSeed || this.outgoingSeedHandler(channelPubkey),
|
|
2447
|
+
preparedPublications: prepared.length ? prepared : undefined,
|
|
2441
2448
|
_getIykcProofs: this.contentKeyLookup()
|
|
2442
2449
|
})
|
|
2443
2450
|
} finally { releaseActivity() }
|
|
2444
2451
|
}
|
|
2445
2452
|
|
|
2446
|
-
async broadcastRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, deletionPubkey, signal, attemptStartedAt, onPreparedSeed, preparedPublication, onDebug }) {
|
|
2453
|
+
async broadcastRumor ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, rumor, deletionPubkey, signal, attemptStartedAt, onPreparedSeed, preparedPublication, preparedPublications, onDebug }) {
|
|
2447
2454
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2448
2455
|
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2449
2456
|
try {
|
|
2450
|
-
const
|
|
2451
|
-
|
|
2457
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
2458
|
+
const receivers = uniq(receiverPubkeys?.length
|
|
2459
|
+
? receiverPubkeys
|
|
2460
|
+
: prepared.flatMap(publication => publication.receiverPubkeys || []))
|
|
2461
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: receivers, relays, relayToReceivers, signal, attemptStartedAt, onDebug })
|
|
2462
|
+
this.debugSend('broadcastRumor', channelPubkey, { receiverPubkeys: receivers })
|
|
2452
2463
|
return await this._privateMessage.broadcastRumor({
|
|
2453
2464
|
senderSigner: this.userSigner,
|
|
2454
2465
|
imkcSigner: this.contentKeySigner,
|
|
2455
2466
|
privateChannelSigner: channel.signer,
|
|
2456
2467
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2457
|
-
receiverPubkeys,
|
|
2468
|
+
receiverPubkeys: receivers,
|
|
2458
2469
|
...routing,
|
|
2459
2470
|
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2460
2471
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
@@ -2462,24 +2473,28 @@ export class PrivateMessenger {
|
|
|
2462
2473
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2463
2474
|
rumor,
|
|
2464
2475
|
onPreparedSeed: onPreparedSeed || this.outgoingSeedHandler(channelPubkey),
|
|
2465
|
-
|
|
2476
|
+
preparedPublications: prepared.length ? prepared : undefined,
|
|
2466
2477
|
_getIykcProofs: this.contentKeyLookup()
|
|
2467
2478
|
})
|
|
2468
2479
|
} finally { releaseActivity() }
|
|
2469
2480
|
}
|
|
2470
2481
|
|
|
2471
|
-
async broadcastEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, deletionPubkey, signal, attemptStartedAt, onPreparedSeed, preparedPublication, onDebug }) {
|
|
2482
|
+
async broadcastEvent ({ channelPubkey = this.defaultChannelPubkey(), receiverPubkeys, relays, relayToReceivers, event, deletionPubkey, signal, attemptStartedAt, onPreparedSeed, preparedPublication, preparedPublications, onDebug }) {
|
|
2472
2483
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
2473
2484
|
const releaseActivity = this.decryptionScheduler.activity(channelPubkey)
|
|
2474
2485
|
try {
|
|
2475
|
-
const
|
|
2476
|
-
|
|
2486
|
+
const prepared = normalizePreparedPublications({ preparedPublication, preparedPublications })
|
|
2487
|
+
const receivers = uniq(receiverPubkeys?.length
|
|
2488
|
+
? receiverPubkeys
|
|
2489
|
+
: prepared.flatMap(publication => publication.receiverPubkeys || []))
|
|
2490
|
+
const routing = await this.resolveSendRouting({ channel, receiverPubkeys: receivers, relays, relayToReceivers, signal, attemptStartedAt, onDebug })
|
|
2491
|
+
this.debugSend('broadcastEvent', channelPubkey, { receiverPubkeys: receivers })
|
|
2477
2492
|
return await this._privateMessage.broadcastEvent({
|
|
2478
2493
|
senderSigner: this.userSigner,
|
|
2479
2494
|
imkcSigner: this.contentKeySigner,
|
|
2480
2495
|
privateChannelSigner: channel.signer,
|
|
2481
2496
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
2482
|
-
receiverPubkeys,
|
|
2497
|
+
receiverPubkeys: receivers,
|
|
2483
2498
|
...routing,
|
|
2484
2499
|
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
2485
2500
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
@@ -2487,7 +2502,7 @@ export class PrivateMessenger {
|
|
|
2487
2502
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
2488
2503
|
event,
|
|
2489
2504
|
onPreparedSeed: onPreparedSeed || this.outgoingSeedHandler(channelPubkey),
|
|
2490
|
-
|
|
2505
|
+
preparedPublications: prepared.length ? prepared : undefined,
|
|
2491
2506
|
_getIykcProofs: this.contentKeyLookup()
|
|
2492
2507
|
})
|
|
2493
2508
|
} finally { releaseActivity() }
|
|
@@ -48,6 +48,11 @@ function replaceErrorMetadata (item, error) {
|
|
|
48
48
|
return Object.assign(next, errorMetadata(error))
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function createdAtMetadata (item) {
|
|
52
|
+
const value = item?.packet?.outer?.created_at ?? item?.outer?.created_at
|
|
53
|
+
return Number.isSafeInteger(value) ? { outerCreatedAt: value } : {}
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
export function pendingContentKeyGroup (item) {
|
|
52
57
|
const receiver = item?.receiverPubkey
|
|
53
58
|
const own = item?.ownContentPubkey
|
|
@@ -92,7 +97,7 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
92
97
|
const notify = (action, detail) => {
|
|
93
98
|
report({ action, ...detail })
|
|
94
99
|
const key = `${action}:${detail.code || detail.reason || ''}`
|
|
95
|
-
counts[key] = { count: (counts[key]?.count || 0) + 1,
|
|
100
|
+
counts[key] = { count: (counts[key]?.count || 0) + 1, sample: detail }
|
|
96
101
|
if (!timer) { timer = setTimeout(flush, 1000); timer.unref?.() }
|
|
97
102
|
}
|
|
98
103
|
return {
|
|
@@ -129,7 +134,16 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
129
134
|
if (evictedBytes) notify('discarded', { reason: 'capacity', bytes: evictedBytes })
|
|
130
135
|
const coordinates = Object.fromEntries(['eventId', 'eventKind', 'senderPubkey', 'receiverPubkey', 'ownContentPubkey', 'peerContentPubkey', 'hasOwnContentKey']
|
|
131
136
|
.filter(key => ['string', 'boolean', 'number'].includes(typeof error[key])).map(key => [key, error[key]]))
|
|
132
|
-
notify('deferred', {
|
|
137
|
+
notify('deferred', {
|
|
138
|
+
...coordinates,
|
|
139
|
+
...createdAtMetadata(record),
|
|
140
|
+
code: error.code,
|
|
141
|
+
stage: error.stage,
|
|
142
|
+
id,
|
|
143
|
+
channelPubkey: item.channelPubkey,
|
|
144
|
+
capacityBytesBefore: before.usedBytes,
|
|
145
|
+
capacityBytesAfter: after.usedBytes
|
|
146
|
+
})
|
|
133
147
|
return true
|
|
134
148
|
})
|
|
135
149
|
},
|
|
@@ -148,7 +162,7 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
148
162
|
recovered (item) {
|
|
149
163
|
return serial(async () => {
|
|
150
164
|
await queue.removeBy('byId', item.id)
|
|
151
|
-
notify('recovered', { id: item.id, channelPubkey: item.channelPubkey })
|
|
165
|
+
notify('recovered', { id: item.id, channelPubkey: item.channelPubkey, ...createdAtMetadata(item) })
|
|
152
166
|
})
|
|
153
167
|
},
|
|
154
168
|
failed (item, error) {
|
|
@@ -161,7 +175,9 @@ export async function createPendingDecryption ({ prefix, indexedDB, maxBytes = P
|
|
|
161
175
|
macAttempts: current.macAttempts + (error.code === 'INVALID_MAC' ? 1 : 0)
|
|
162
176
|
}
|
|
163
177
|
await queue.putBy('byId', updated, { existingOnly: true })
|
|
164
|
-
if (updated.macAttempts >= 3 && item.macAttempts < 3)
|
|
178
|
+
if (updated.macAttempts >= 3 && item.macAttempts < 3) {
|
|
179
|
+
notify('suspended', { id: item.id, code: error.code, channelPubkey: item.channelPubkey, ...createdAtMetadata(updated) })
|
|
180
|
+
}
|
|
165
181
|
})
|
|
166
182
|
},
|
|
167
183
|
clearChannel: channel => serial(() => queue.removeBy('byChannel', channel)),
|
|
@@ -289,9 +289,17 @@ recipient/channel and inner-event identities, event/chunk position, retention
|
|
|
289
289
|
and accepted/archived flags. Old entries without it retain `localSaved`, index
|
|
290
290
|
and file/chunk progress and enter the same preparation path; there is no database
|
|
291
291
|
migration or alternate publisher. Replays validate signatures and identity and
|
|
292
|
-
reuse the exact envelopes without repeating cryptography or extending expiry
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
reuse the exact envelopes without repeating cryptography or extending expiry,
|
|
293
|
+
except for one 3560 age-auth refresh. That refresh reuses router/ciphertext and
|
|
294
|
+
seeds but re-signs the whole publication with a new shared `created_at` and
|
|
295
|
+
recomputed `expiration`, writes the replacement generation before retrying, and
|
|
296
|
+
sets `outerRefreshAttempted` so reload cannot repeat it. A relay may report a
|
|
297
|
+
stale wrapper as `invalid:`; that prefix permits this refresh but does not
|
|
298
|
+
attempt NIP-42 authentication.
|
|
299
|
+
|
|
300
|
+
The existing `onPreparedSeed` callback now also supplies `publication`, plus an
|
|
301
|
+
optional `{ replace: true }` second argument when a refreshed publication must
|
|
302
|
+
replace the same inner-event/receiver group in place. The
|
|
295
303
|
session commits that minimal journal before transmitting; direct messengers
|
|
296
304
|
retain their local seed-store durability. Only after that commit can relay
|
|
297
305
|
publication proceed. One independent serial worker converts recovery seeds into
|
|
@@ -321,6 +329,12 @@ relay confirmation separately from `localSaved`. Old entries normalize from
|
|
|
321
329
|
saved delivery status, never from local progress; their event/file indices and
|
|
322
330
|
prepared envelopes remain unchanged. No database migration is needed.
|
|
323
331
|
|
|
332
|
+
Prepared envelopes are stored as the `prepared.publications` array. Older
|
|
333
|
+
snapshots containing `prepared.publication` are converted to a one-item array
|
|
334
|
+
and rewritten through the same encrypted outbox queue before publication or
|
|
335
|
+
archive work resumes. The singular key is never written by current versions and
|
|
336
|
+
is not accepted as a public replay argument.
|
|
337
|
+
|
|
324
338
|
`onOutbox` adds `localPersistence: { state, retryable }`, where state is pending,
|
|
325
339
|
saved or failed. It is a presentation summary with no native error objects.
|
|
326
340
|
A saved remote entry may still have pending/failed local work. Keep it visible
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationError } from '../../../error/index.js'
|
|
2
|
+
|
|
3
|
+
export function normalizePreparedState (prepared) {
|
|
4
|
+
if (!prepared) return { prepared, migrated: false }
|
|
5
|
+
const legacy = prepared.publication
|
|
6
|
+
const publications = prepared.publications !== undefined
|
|
7
|
+
? prepared.publications
|
|
8
|
+
: legacy !== undefined
|
|
9
|
+
? [legacy]
|
|
10
|
+
: null
|
|
11
|
+
if (!Array.isArray(publications) || publications.length === 0) {
|
|
12
|
+
throw new ValidationError('INVALID_PREPARED_PUBLICATIONS')
|
|
13
|
+
}
|
|
14
|
+
prepared.publications = publications
|
|
15
|
+
delete prepared.publication
|
|
16
|
+
return { prepared, migrated: legacy !== undefined }
|
|
17
|
+
}
|
|
@@ -24,7 +24,7 @@ export function createPublicationJournal ({ entries, store, archive, changed, on
|
|
|
24
24
|
function rows (entry) {
|
|
25
25
|
const packet = entry.prepared
|
|
26
26
|
if (!packet?.recover) return []
|
|
27
|
-
return packet.publication.seeds.flatMap(seed => compactSeedRouterRows(seed).map(row => {
|
|
27
|
+
return packet.publications.flatMap(publication => publication.seeds.flatMap(seed => compactSeedRouterRows(seed).map(row => {
|
|
28
28
|
const descriptor = packet.target.file
|
|
29
29
|
const value = {
|
|
30
30
|
...row, channelPubkey: seed.channelPubkey, receivedAt: packet.receivedAt,
|
|
@@ -32,15 +32,15 @@ export function createPublicationJournal ({ entries, store, archive, changed, on
|
|
|
32
32
|
...(descriptor
|
|
33
33
|
? {
|
|
34
34
|
root: descriptor.root, controlChannelPubkey: descriptor.controlChannelPubkey,
|
|
35
|
-
fileChannelPubkey:
|
|
35
|
+
fileChannelPubkey: publication.channelPubkey, peerPubkey: descriptor.peerPubkey,
|
|
36
36
|
chunkIndex: packet.target.chunkIndex,
|
|
37
|
-
key: `${descriptor.controlChannelPubkey}:${
|
|
37
|
+
key: `${descriptor.controlChannelPubkey}:${publication.channelPubkey}:${row.receiverPubkey}:${packet.target.chunkIndex}`
|
|
38
38
|
}
|
|
39
39
|
: {})
|
|
40
40
|
}
|
|
41
41
|
value.recordId = encodeRecord('seed', value).tags[0][1]
|
|
42
42
|
return value
|
|
43
|
-
}))
|
|
43
|
+
})))
|
|
44
44
|
}
|
|
45
45
|
function retryLater () {
|
|
46
46
|
if (stopped || !enabled || timer || !failed.size) return
|
|
@@ -60,7 +60,7 @@ export function createPublicationJournal ({ entries, store, archive, changed, on
|
|
|
60
60
|
try {
|
|
61
61
|
if (!durable.has(packet)) { await write(entry); durable.add(packet) }
|
|
62
62
|
if (!packet.archived) {
|
|
63
|
-
debug(entry, 'recovery-archive-start', queued.get(packet), { publicationId: packet.
|
|
63
|
+
debug(entry, 'recovery-archive-start', queued.get(packet), { publicationId: packet.publications[0].events[0].id })
|
|
64
64
|
if (packet.recover) await archive(entry, rows(entry))
|
|
65
65
|
packet.archived = true
|
|
66
66
|
debug(entry, 'recovery-archived', started)
|
|
@@ -87,15 +87,43 @@ export function createPublicationJournal ({ entries, store, archive, changed, on
|
|
|
87
87
|
write,
|
|
88
88
|
async flush (entry) { while (writes.has(entry.id)) await writes.get(entry.id) },
|
|
89
89
|
adopt (entry) { if (entry.prepared) { durable.add(entry.prepared); queued.set(entry.prepared, Date.now()) } },
|
|
90
|
-
async stage (entry, seed, target, retentionSeconds) {
|
|
90
|
+
async stage (entry, seed, target, retentionSeconds, { replace = false } = {}) {
|
|
91
91
|
const started = Date.now()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
const publication = structuredClone(seed.publication)
|
|
93
|
+
const targetKey = JSON.stringify(target)
|
|
94
|
+
entry.prepared ||= { publications: [], target, receivedAt: Math.floor(Date.now() / 1000), retentionSeconds, recover: retentionSeconds > 0, accepted: false, archived: retentionSeconds <= 0 }
|
|
95
|
+
if (replace && targetKey !== JSON.stringify(entry.prepared.target)) throw new Error('PUBLICATION_ARCHIVE_TARGET_MISMATCH')
|
|
96
|
+
if (entry.prepared.publications.some(existing => existing.innerEventId !== publication.innerEventId)) {
|
|
97
|
+
throw new Error('PUBLICATION_ARCHIVE_PENDING')
|
|
98
|
+
}
|
|
99
|
+
const receiverKey = (publication.receiverPubkeys || []).slice().sort().join(',')
|
|
100
|
+
const eventKey = publication.events.map(event => event.id).join(',')
|
|
101
|
+
const matchingIndexes = entry.prepared.publications
|
|
102
|
+
.map((existing, index) => ({ existing, index }))
|
|
103
|
+
.filter(({ existing }) => existing.innerEventId === publication.innerEventId &&
|
|
104
|
+
(existing.receiverPubkeys || []).slice().sort().join(',') === receiverKey)
|
|
105
|
+
const existingIndex = matchingIndexes[0]?.index ?? -1
|
|
106
|
+
const existing = matchingIndexes[0]?.existing
|
|
107
|
+
const duplicate = Boolean(existing) &&
|
|
108
|
+
existing.events.map(event => event.id).join(',') === eventKey
|
|
109
|
+
const matches = existingIndex >= 0
|
|
110
|
+
if (replace) {
|
|
111
|
+
if (!matches) throw new Error('PUBLICATION_ARCHIVE_REPLACE_MISSING')
|
|
112
|
+
entry.prepared.publications = entry.prepared.publications
|
|
113
|
+
.filter((_publication, index) => index !== existingIndex)
|
|
114
|
+
.filter(candidate =>
|
|
115
|
+
candidate.innerEventId !== publication.innerEventId ||
|
|
116
|
+
(candidate.receiverPubkeys || []).slice().sort().join(',') !== receiverKey
|
|
117
|
+
)
|
|
118
|
+
entry.prepared.publications.splice(existingIndex, 0, publication)
|
|
119
|
+
} else {
|
|
120
|
+
if (!duplicate) entry.prepared.publications.push(publication)
|
|
121
|
+
if (duplicate && durable.has(entry.prepared)) return
|
|
122
|
+
}
|
|
95
123
|
queued.set(entry.prepared, Date.now())
|
|
96
124
|
await write(entry)
|
|
97
125
|
durable.add(entry.prepared)
|
|
98
|
-
debug(entry, 'publication-checkpoint', started, { publicationId: seed.publication.events[0].id })
|
|
126
|
+
debug(entry, replace ? 'publication-refresh' : 'publication-checkpoint', started, { publicationId: seed.publication.events[0].id })
|
|
99
127
|
for (const row of rows(entry)) debug(entry, 'recovery-pending', undefined, { seedRecordId: row.recordId, publicationId: seed.publication.events[0].id })
|
|
100
128
|
wake()
|
|
101
129
|
},
|
|
@@ -16,6 +16,7 @@ import { assertMessagePublished } from '../helpers/publication.js'
|
|
|
16
16
|
import { normalizeFallbackRelays, normalizeFallbackDelay } from '../helpers/send-routing.js'
|
|
17
17
|
import { createPublicationJournal, withPendingSeeds } from './helpers/publication-journal.js'
|
|
18
18
|
import { createPauseRecovery, isRecoverableStorageFailure } from '../helpers/pause-recovery.js'
|
|
19
|
+
import { normalizePreparedState } from './helpers/prepared-state.js'
|
|
19
20
|
|
|
20
21
|
export function wireEvent (value, owner) {
|
|
21
22
|
const event = { kind: value.kind, created_at: value.created_at, tags: structuredClone(value.tags), content: value.content, pubkey: value.pubkey || owner }
|
|
@@ -164,6 +165,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
164
165
|
const idleWaiters = new Set()
|
|
165
166
|
const settled = () => { if (!sending && !localSaving && !enqueuing && !draining && !cleanups.size) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
166
167
|
const localComplete = entry => entry.localSaved.every(Boolean)
|
|
168
|
+
const preparedPublicationId = entry => entry.prepared?.publications?.[0]?.events?.[0]?.id
|
|
167
169
|
const completed = entry => !unadmitted.has(entry) && localComplete(entry) && (entry.peer === owner || entry.deliveryConfirmed)
|
|
168
170
|
const emit = () => onOutbox([...entries.values()].filter(entry => !unadmitted.has(entry) && !entry.deletion && !entry.cancelled).map(entry => ({
|
|
169
171
|
...entry,
|
|
@@ -188,7 +190,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
188
190
|
const onDiagnostic = details => {
|
|
189
191
|
diagnosticReported = true
|
|
190
192
|
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
191
|
-
publicationId: entry
|
|
193
|
+
publicationId: preparedPublicationId(entry),
|
|
192
194
|
...details
|
|
193
195
|
})
|
|
194
196
|
}
|
|
@@ -199,15 +201,19 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
199
201
|
for (const row of rows) archived.push(await recoveryStorage.seeds.put(row))
|
|
200
202
|
}
|
|
201
203
|
} else if (entry.prepared.target.file) {
|
|
202
|
-
for (const
|
|
204
|
+
for (const publication of entry.prepared.publications) {
|
|
205
|
+
for (const seed of publication.seeds) await fileTransfers.archiveSeed(entry.prepared.target.file, seed)
|
|
206
|
+
}
|
|
203
207
|
} else {
|
|
204
|
-
for (const
|
|
208
|
+
for (const publication of entry.prepared.publications) {
|
|
209
|
+
for (const seed of publication.seeds) await messenger.enqueueSeed(seed.channelPubkey, seed)
|
|
210
|
+
}
|
|
205
211
|
}
|
|
206
212
|
if (!diagnosticReported) {
|
|
207
213
|
const results = Array.isArray(archived) ? archived : []
|
|
208
214
|
const storedCount = results.filter(Boolean).length
|
|
209
215
|
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
210
|
-
publicationId: entry
|
|
216
|
+
publicationId: preparedPublicationId(entry),
|
|
211
217
|
rowCount: rows.length,
|
|
212
218
|
uniqueCount: new Set(rows.map(row => row.recordId).filter(Boolean)).size,
|
|
213
219
|
storedCount,
|
|
@@ -226,6 +232,9 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
226
232
|
const ready = () => (initialized ??= (async () => {
|
|
227
233
|
storage = await openOutbox({ owner, signer, onDebug })
|
|
228
234
|
for (const entry of await storage.list()) {
|
|
235
|
+
if (normalizePreparedState(entry.prepared).migrated) {
|
|
236
|
+
await storage.put(structuredClone(entry), { existing: true })
|
|
237
|
+
}
|
|
229
238
|
// Old encrypted entries retain progress and enter this same pipeline.
|
|
230
239
|
const legacyLocalFailure = entry.deliveryConfirmed === undefined && entry.failed && !entry.localSaved.every(Boolean)
|
|
231
240
|
if (legacyLocalFailure) {
|
|
@@ -471,10 +480,10 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
471
480
|
const options = {
|
|
472
481
|
channelPubkey, receiverPubkeys: [peer], signal, attemptStartedAt: attemptStarts.get(entry.id),
|
|
473
482
|
onDebug: onDebug ? value => debug(entry, value.stage, undefined, value) : undefined,
|
|
474
|
-
|
|
475
|
-
onPreparedSeed: async seed => {
|
|
483
|
+
preparedPublications: entry.prepared?.publications,
|
|
484
|
+
onPreparedSeed: async (seed, { replace = false } = {}) => {
|
|
476
485
|
debug(entry, 'envelope-prepared', preparationStartedAt, { publicationId: seed.publication.events[0].id })
|
|
477
|
-
await journal.stage(entry, seed, { index: entry.index }, retention)
|
|
486
|
+
await journal.stage(entry, seed, { index: entry.index }, retention, { replace })
|
|
478
487
|
signal.throwIfAborted()
|
|
479
488
|
}
|
|
480
489
|
}
|
|
@@ -592,8 +601,11 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
592
601
|
const recover = mode === 'watchtower' ? messenger.offlineRecoverySecondsFor?.(descriptor.controlChannelPubkey) ?? 0 : 0
|
|
593
602
|
const target = { file: descriptor, fileIndex: entry.fileIndex, chunkIndex: entry.chunkIndex }
|
|
594
603
|
await fileTransfers.publishChunk(descriptor, wireEvent(chunk, owner), {
|
|
595
|
-
signal: controller.signal, attemptStartedAt: chunkStartedAt, onDebug: onDebug ? value => debug(entry, value.stage, undefined, value) : undefined,
|
|
596
|
-
onPreparedSeed: async seed
|
|
604
|
+
signal: controller.signal, attemptStartedAt: chunkStartedAt, onDebug: onDebug ? value => debug(entry, value.stage, undefined, value) : undefined, preparedPublications: entry.prepared?.publications,
|
|
605
|
+
onPreparedSeed: async (seed, { replace = false } = {}) => {
|
|
606
|
+
await journal.stage(entry, seed, target, recover, { replace })
|
|
607
|
+
controller.signal.throwIfAborted()
|
|
608
|
+
}
|
|
597
609
|
})
|
|
598
610
|
entry.chunkIndex++
|
|
599
611
|
const completed = entry.files.slice(0, entry.fileIndex).reduce((sum, value) => sum + value.size, 0) + Math.min(file.size, entry.chunkIndex * IRFS_CHUNK_BYTES)
|