libp2r2p 0.11.26 → 0.11.27
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
CHANGED
|
@@ -1252,3 +1252,8 @@ uploads budget each chunk independently. Monotonic clocks never enter storage or
|
|
|
1252
1252
|
Nostr events. Omitted timestamps keep the publication-local delay.
|
|
1253
1253
|
`RelayPool.sendEvent` accepts a consumer `signal` without closing shared sockets.
|
|
1254
1254
|
See [fallback and cancellation](private-messenger/session/README.md#relay-fallback).
|
|
1255
|
+
|
|
1256
|
+
The 0.11.27 session publishes independently of local event-store persistence after
|
|
1257
|
+
its initial durable intent and exact-envelope checkpoint. `deliveryConfirmed`
|
|
1258
|
+
and the copied `localPersistence` summary distinguish relay acceptance from
|
|
1259
|
+
local storage. See `private-messenger/session/README.md` for retry and cleanup.
|
package/package.json
CHANGED
|
@@ -77,7 +77,8 @@ Callbacks are optional `onDebug`, `onOutbox`, `onMedia`,
|
|
|
77
77
|
account permissions/connectivity/lock state to availability and owns view subscriptions. There are no DOM or UI imports.
|
|
78
78
|
|
|
79
79
|
`onSendError` identifies the main outbox message even when its quote, metadata,
|
|
80
|
-
file chunks, or
|
|
80
|
+
file chunks, or self-chat persistence failed. Remote local-persistence failures
|
|
81
|
+
use onError and the copied localPersistence summary, never a send failure. The original error is also reported to
|
|
81
82
|
`onError` unchanged. It fires once per failed send attempt (including retries),
|
|
82
83
|
not when persisted failures are read, nor for inbox/download/deletion operations
|
|
83
84
|
or cancelled/closed sends. Apps own route visibility and user-intent filtering;
|
|
@@ -233,10 +234,10 @@ creates a fresh publication lifetime, without treating the pause as a user abort
|
|
|
233
234
|
## Independent local persistence and publication
|
|
234
235
|
|
|
235
236
|
One encrypted outbox feeds two independent serial processors. Local persistence
|
|
236
|
-
never waits for peer preparation or an in-flight publication.
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
never waits for peer preparation or an in-flight publication. After the initial encrypted intent commits, remote preparation and publication
|
|
238
|
+
never wait for event-store persistence. Only the exact prepared-envelope
|
|
239
|
+
checkpoint gates transmission. Text continues passing uploads between chunks;
|
|
240
|
+
its local stage need not have completed. Both stages use the admitted inner ID.
|
|
240
241
|
|
|
241
242
|
Self entries emit `status: 'saved'` as soon as local persistence succeeds. They
|
|
242
243
|
do not write a redundant localSaved checkpoint before removal. Remote entries
|
|
@@ -275,7 +276,7 @@ First acceptance of every required envelope emits saved independently of archive
|
|
|
275
276
|
or cleanup. ACK progress checkpoints serialize with other entry writes. A crash
|
|
276
277
|
between ACK and its checkpoint may replay the same envelope; acknowledged durable
|
|
277
278
|
progress only archives. Archive failure never becomes `onSendError` or changes
|
|
278
|
-
saved status. Removal waits for archival and outstanding entry writes.
|
|
279
|
+
saved status. Removal waits for local persistence, archival and outstanding entry writes.
|
|
279
280
|
|
|
280
281
|
At most one unit awaits archival per entry; a stalled upload yields its remote
|
|
281
282
|
slot to other texts rather than accumulating the entire file. Cancel stops wire
|
|
@@ -283,3 +284,35 @@ work but retains potentially transmitted seeds until archival finishes. Close
|
|
|
283
284
|
drains active local operations and leaves durable backoff work for restart.
|
|
284
285
|
Diagnostics add publication/seed IDs, checkpoint time, archive wait/execution and
|
|
285
286
|
first-ACK timing, without message or ciphertext contents.
|
|
287
|
+
|
|
288
|
+
## Independent delivery status (0.11.27)
|
|
289
|
+
|
|
290
|
+
`deliveryConfirmed` is an optional boolean in encrypted outbox JSON. It records
|
|
291
|
+
relay confirmation separately from `localSaved`. Old entries normalize from
|
|
292
|
+
saved delivery status, never from local progress; their event/file indices and
|
|
293
|
+
prepared envelopes remain unchanged. No database migration is needed.
|
|
294
|
+
|
|
295
|
+
`onOutbox` adds `localPersistence: { state, retryable }`, where state is pending,
|
|
296
|
+
saved or failed. It is a presentation summary with no native error objects.
|
|
297
|
+
A saved remote entry may still have pending/failed local work. Keep it visible
|
|
298
|
+
through reload until the local store catches up. `retry(id)` on a confirmed
|
|
299
|
+
entry repeats local/archival cleanup only and never renews network urgency.
|
|
300
|
+
Recoverable local failures retain bounded backoff without clearing terminal
|
|
301
|
+
remote errors or releasing network pauses. Self-chat still confirms on commit.
|
|
302
|
+
|
|
303
|
+
Entry updates serialize through existing-only checkpoints; not-yet-started
|
|
304
|
+
writes coalesce and capture the latest monotonic progress at transaction start.
|
|
305
|
+
Prepared-publication callers wait for the resulting durable snapshot. An ACK
|
|
306
|
+
checkpoint may merge local progress but never requires it. Shutdown/lock waits
|
|
307
|
+
for admitted local writes; cancellation cannot recreate a removed intent.
|
|
308
|
+
|
|
309
|
+
The local failure summary remains failed while a retry is queued or running;
|
|
310
|
+
unrelated snapshots cannot hide its warning before commit. Retry admission is
|
|
311
|
+
runtime-only and temporarily makes that summary non-retryable. A failed retry
|
|
312
|
+
restores retryability after its operation ends; a successful commit clears the
|
|
313
|
+
failure. This does not change the already confirmed remote status.
|
|
314
|
+
|
|
315
|
+
Legacy single-stage failure flags with incomplete local progress normalize into
|
|
316
|
+
local failure state, preserving manual-only retry refusals. The new delivery
|
|
317
|
+
marker distinguishes those records from a modern remote failure racing local
|
|
318
|
+
persistence. Both formats enter the same processors after normalization.
|
|
@@ -4,13 +4,19 @@ import { isRecoverableStorageFailure } from '../../helpers/pause-recovery.js'
|
|
|
4
4
|
|
|
5
5
|
export function createPublicationJournal ({ entries, store, archive, changed, onError, debug, setTimer = setTimeout, clearTimer = clearTimeout, random = Math.random }) {
|
|
6
6
|
const writes = new Map()
|
|
7
|
+
const waitingWrites = new Map()
|
|
7
8
|
const failed = new Set()
|
|
8
9
|
const durable = new WeakSet()
|
|
9
10
|
const queued = new WeakMap()
|
|
10
11
|
let working; let stopped = false; let enabled = false; let timer; let delay = 1000
|
|
11
12
|
function write (entry) {
|
|
13
|
+
if (waitingWrites.has(entry.id)) return waitingWrites.get(entry.id)
|
|
12
14
|
const previous = writes.get(entry.id) || Promise.resolve()
|
|
13
|
-
const work = previous.catch(() => {}).then(() =>
|
|
15
|
+
const work = previous.catch(() => {}).then(() => {
|
|
16
|
+
waitingWrites.delete(entry.id)
|
|
17
|
+
return store().put(structuredClone(entry), { existing: true })
|
|
18
|
+
})
|
|
19
|
+
waitingWrites.set(entry.id, work)
|
|
14
20
|
writes.set(entry.id, work)
|
|
15
21
|
work.finally(() => { if (writes.get(entry.id) === work) writes.delete(entry.id) }).catch(() => {})
|
|
16
22
|
return work
|
|
@@ -69,8 +69,9 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
69
69
|
const attemptStarts = new Map()
|
|
70
70
|
const networkUrgency = new Map()
|
|
71
71
|
const sendControllers = new Map()
|
|
72
|
-
const localReady = new Set()
|
|
73
72
|
const localIdleWaiters = new Set()
|
|
73
|
+
// Last local failure survives retry admission until an actual commit.
|
|
74
|
+
const localRetries = new Set()
|
|
74
75
|
const cleanups = new Map()
|
|
75
76
|
const queuedAt = new WeakMap()
|
|
76
77
|
const debug = (entry, stage, startedAt, coordinates = {}) => {
|
|
@@ -104,13 +105,14 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
104
105
|
for (const entry of entries.values()) {
|
|
105
106
|
if (entry.peer !== owner && entry.failed && entry.status === 'pending') pausedSends.add(entry.id)
|
|
106
107
|
}
|
|
107
|
-
if (![...entries.values()].some(entry => pendingRetryable(entry) && !pausedSends.has(entry.id))) clearSendRetry()
|
|
108
|
+
if (![...entries.values()].some(entry => (entry.localFailed && entry.localRetryable) || (pendingRetryable(entry) && !pausedSends.has(entry.id)))) clearSendRetry()
|
|
108
109
|
watchOfflineSends()
|
|
109
110
|
return
|
|
110
111
|
}
|
|
111
112
|
clearSendRetry()
|
|
112
113
|
for (const id of pausedSends) { const entry = entries.get(id); if (entry) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) } }
|
|
113
114
|
pausedSends.clear()
|
|
115
|
+
if ([...entries.values()].some(entry => entry.localFailed && entry.localRetryable)) scheduleSendRetry()
|
|
114
116
|
watchOfflineSends()
|
|
115
117
|
// The callback can run inside construction/resume, before configure settles.
|
|
116
118
|
queueMicrotask(() => pump())
|
|
@@ -153,7 +155,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
153
155
|
let redrain = false
|
|
154
156
|
const idleWaiters = new Set()
|
|
155
157
|
const settled = () => { if (!sending && !localSaving && !enqueuing && !draining && !cleanups.size) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
156
|
-
const
|
|
158
|
+
const localComplete = entry => entry.localSaved.every(Boolean)
|
|
159
|
+
const completed = entry => localComplete(entry) && (entry.peer === owner || entry.deliveryConfirmed)
|
|
160
|
+
const emit = () => onOutbox([...entries.values()].filter(entry => !entry.deletion && !entry.cancelled).map(entry => ({
|
|
161
|
+
...entry,
|
|
162
|
+
localPersistence: { state: localComplete(entry) ? 'saved' : entry.localFailed ? 'failed' : 'pending', retryable: Boolean(entry.localFailed && entry.localRetryable && !localRetries.has(entry.id)) }
|
|
163
|
+
})))
|
|
157
164
|
const journal = createPublicationJournal({
|
|
158
165
|
entries, store: () => storage, onError: reportError, debug, setTimer: _setTimeout, clearTimer: _clearTimeout, random: _random,
|
|
159
166
|
archive: async (entry, rows) => {
|
|
@@ -165,7 +172,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
165
172
|
},
|
|
166
173
|
changed: entry => {
|
|
167
174
|
watchOfflineSends()
|
|
168
|
-
if (!entry.prepared && (entry
|
|
175
|
+
if (!entry.prepared && (completed(entry) || entry.cancelled)) cleanupEntry(entry)
|
|
169
176
|
else if (!closed && available) pumpRemote()
|
|
170
177
|
}
|
|
171
178
|
})
|
|
@@ -174,11 +181,17 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
174
181
|
storage = await openOutbox({ owner, signer, onDebug })
|
|
175
182
|
for (const entry of await storage.list()) {
|
|
176
183
|
// Old encrypted entries retain progress and enter this same pipeline.
|
|
184
|
+
const legacyLocalFailure = entry.deliveryConfirmed === undefined && entry.failed && !entry.localSaved.every(Boolean)
|
|
185
|
+
if (legacyLocalFailure) {
|
|
186
|
+
entry.localFailed = true; entry.localRetryable = Boolean(entry.retryable || entry.status === 'pending')
|
|
187
|
+
if (entry.peer !== owner) { entry.failed = false; entry.status = 'pending' }
|
|
188
|
+
}
|
|
189
|
+
entry.deliveryConfirmed ??= entry.peer !== owner && entry.status === 'saved'
|
|
190
|
+
if (entry.deliveryConfirmed) entry.status = 'saved'
|
|
177
191
|
if (entry.cancelled) cancelled.add(entry.id)
|
|
178
192
|
entries.set(entry.id, entry)
|
|
179
193
|
journal.adopt(entry)
|
|
180
194
|
attemptStarts.set(entry.id, performance.now())
|
|
181
|
-
if (entry.localSaved.every(Boolean)) localReady.add(entry.id)
|
|
182
195
|
queuedAt.set(entry, Date.now())
|
|
183
196
|
}
|
|
184
197
|
emit()
|
|
@@ -432,10 +445,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
432
445
|
}
|
|
433
446
|
async function cleanupEntry (entry) {
|
|
434
447
|
if (entry.prepared) { journal.wake(); return }
|
|
448
|
+
if (!entry.cancelled && !completed(entry)) return
|
|
449
|
+
if (entry.failed && entry.status === 'saved') return
|
|
435
450
|
if (cleanups.has(entry.id)) return cleanups.get(entry.id)
|
|
436
451
|
const startedAt = Date.now()
|
|
437
452
|
const work = Promise.resolve().then(async () => { await journal.flush(entry); await storage.remove(entry.id) }).then(() => {
|
|
438
|
-
entries.delete(entry.id);
|
|
453
|
+
entries.delete(entry.id); localRetries.delete(entry.id); attemptStarts.delete(entry.id)
|
|
439
454
|
debug(entry, 'outbox-cleanup', startedAt)
|
|
440
455
|
emit()
|
|
441
456
|
}).catch(error => {
|
|
@@ -447,7 +462,49 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
447
462
|
cleanups.set(entry.id, work)
|
|
448
463
|
return work
|
|
449
464
|
}
|
|
450
|
-
async function
|
|
465
|
+
async function saveEntry (entry) {
|
|
466
|
+
if (entry.peer === owner) { entry.status = 'pending'; emit() }
|
|
467
|
+
const releaseForeground = foreground.enter()
|
|
468
|
+
const active = async () => {
|
|
469
|
+
if (storage.has && !await storage.has(entry.id)) { cancelled.add(entry.id); sendControllers.get(entry.id)?.abort() }
|
|
470
|
+
if (closed || !available || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
471
|
+
}
|
|
472
|
+
const startedAt = Date.now()
|
|
473
|
+
try {
|
|
474
|
+
debug(entry, 'local-start', queuedAt.get(entry))
|
|
475
|
+
for (let index = 0; index < entry.events.length; index++) {
|
|
476
|
+
await active()
|
|
477
|
+
if (entry.localSaved[index]) continue
|
|
478
|
+
const event = entry.events[index]
|
|
479
|
+
const hearsay = !event.sig && event.pubkey !== owner
|
|
480
|
+
const result = await messageStorage.save(event, { peerPubkey: entry.peer, hearsay })
|
|
481
|
+
if (result?.result?.code === 'blocked') { cancelled.add(entry.id); entry.cancelled = true; sendControllers.get(entry.id)?.abort(); await active() }
|
|
482
|
+
if (!result?.result?.ok) throw Object.assign(new Error('MESSAGE_STORAGE_FAILED'), { code: result?.result?.code?.toUpperCase() })
|
|
483
|
+
entry.localSaved[index] = true
|
|
484
|
+
}
|
|
485
|
+
entry.localFailed = false
|
|
486
|
+
debug(entry, 'local-stored', startedAt)
|
|
487
|
+
if (entry.peer === owner) entry.status = 'saved'
|
|
488
|
+
emit()
|
|
489
|
+
if (completed(entry)) cleanupEntry(entry)
|
|
490
|
+
else await journal.write(entry)
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (cancelled.has(entry.id)) { sendControllers.get(entry.id)?.abort(); entry.cancelled = true; cleanupEntry(entry) } else if (!closed && available) {
|
|
493
|
+
entry.localFailed = true; entry.localRetryable = retryable(error)
|
|
494
|
+
if (entry.peer === owner) { entry.status = 'error'; entry.retryable = entry.localRetryable }
|
|
495
|
+
await journal.write(entry).catch(reportError)
|
|
496
|
+
reportError(error)
|
|
497
|
+
if (entry.localRetryable) scheduleSendRetry()
|
|
498
|
+
if (entry.peer === owner && !entry.deletion) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
499
|
+
emit()
|
|
500
|
+
}
|
|
501
|
+
} finally {
|
|
502
|
+
const retried = localRetries.delete(entry.id)
|
|
503
|
+
releaseForeground()
|
|
504
|
+
if (retried && entry.localFailed) emit()
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function sendEntry (entry) {
|
|
451
508
|
if (entry.status === 'saved') return
|
|
452
509
|
const releaseForeground = foreground.enter()
|
|
453
510
|
entry.status = 'pending'; emit()
|
|
@@ -455,36 +512,9 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
455
512
|
sendControllers.set(entry.id, controller)
|
|
456
513
|
try {
|
|
457
514
|
const active = async () => {
|
|
458
|
-
if (storage.has && !await storage.has(entry.id)) cancelled.add(entry.id)
|
|
515
|
+
if (storage.has && !await storage.has(entry.id)) { cancelled.add(entry.id); sendControllers.get(entry.id)?.abort() }
|
|
459
516
|
if (closed || !available || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
460
517
|
}
|
|
461
|
-
if (localOnly) {
|
|
462
|
-
debug(entry, 'local-start', queuedAt.get(entry))
|
|
463
|
-
const startedAt = Date.now()
|
|
464
|
-
for (let index = 0; index < entry.events.length; index++) {
|
|
465
|
-
await active()
|
|
466
|
-
if (entry.localSaved[index]) continue
|
|
467
|
-
const event = entry.events[index]
|
|
468
|
-
const hearsay = !event.sig && event.pubkey !== owner
|
|
469
|
-
const result = await messageStorage.save(event, { peerPubkey: entry.peer, hearsay })
|
|
470
|
-
if (result?.result?.code === 'blocked') { cancelled.add(entry.id); await active() }
|
|
471
|
-
if (!result?.result?.ok) throw Object.assign(new Error('MESSAGE_STORAGE_FAILED'), { code: result?.result?.code?.toUpperCase() })
|
|
472
|
-
entry.localSaved[index] = true
|
|
473
|
-
}
|
|
474
|
-
debug(entry, 'local-stored', startedAt)
|
|
475
|
-
await active()
|
|
476
|
-
if (entry.peer === owner) {
|
|
477
|
-
entry.status = 'saved'; emit()
|
|
478
|
-
cleanupEntry(entry)
|
|
479
|
-
} else {
|
|
480
|
-
// Retry this checkpoint even when a preceding attempt saved all copies
|
|
481
|
-
// but failed to persist progress. Only then admit remote publication.
|
|
482
|
-
await journal.write(entry)
|
|
483
|
-
await active()
|
|
484
|
-
localReady.add(entry.id)
|
|
485
|
-
}
|
|
486
|
-
return
|
|
487
|
-
}
|
|
488
518
|
if (entry.peer !== owner && !preparation.get(entry.peer)) {
|
|
489
519
|
if (preparation.error(entry.peer)) throw preparation.error(entry.peer)
|
|
490
520
|
preparation.request(entry.peer).catch(() => {})
|
|
@@ -518,14 +548,10 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
518
548
|
await active()
|
|
519
549
|
if (entry.prepared) { journal.accept(entry); return }
|
|
520
550
|
await journal.write(entry)
|
|
521
|
-
if ([...entries.values()].some(pending => !pending.files.length && pending.peer !== owner && !pending.failed && !localReady.has(pending.id))) {
|
|
522
|
-
await pumpLocal()
|
|
523
|
-
if (localSaving) await new Promise(resolve => localIdleWaiters.add(resolve))
|
|
524
|
-
}
|
|
525
551
|
let admitted = 0
|
|
526
552
|
for (const pending of entries.values()) {
|
|
527
553
|
if (closed || !available || admitted >= 8) break
|
|
528
|
-
if (
|
|
554
|
+
if (pending.peer === owner || pending.status === 'saved' || pending.files.length || pending.failed || cancelled.has(pending.id) || pending.prepared?.accepted || !peers.has(pending.peer)) continue
|
|
529
555
|
admitted++
|
|
530
556
|
await sendEntry(pending)
|
|
531
557
|
}
|
|
@@ -539,21 +565,23 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
539
565
|
debug(entry, 'publication-accepted', startedAt)
|
|
540
566
|
await active()
|
|
541
567
|
entry.index++
|
|
542
|
-
if (entry.index === entry.events.length) { entry.status = 'saved'; emit() }
|
|
568
|
+
if (entry.index === entry.events.length) { entry.deliveryConfirmed = true; entry.status = 'saved'; emit() }
|
|
543
569
|
if (entry.prepared) { journal.accept(entry); return }
|
|
544
570
|
if (entry.index < entry.events.length) await journal.write(entry)
|
|
545
571
|
}
|
|
546
|
-
entry.status = 'saved'; emit()
|
|
572
|
+
entry.deliveryConfirmed = true; entry.status = 'saved'; emit()
|
|
573
|
+
await journal.write(entry)
|
|
547
574
|
cleanupEntry(entry)
|
|
548
575
|
} catch (cause) {
|
|
549
576
|
// Pauses can surface at any stage (text publication, file authorization
|
|
550
577
|
// or chunk delivery). Normalize all of them into the retryable path.
|
|
551
|
-
const error = cause?.retryWhenAvailable ? cause : (isUnavailableSend(cause) ? unavailableError(cause, controller.signal) : cause)
|
|
578
|
+
const error = !available || closed ? unavailableError(cause) : cause?.retryWhenAvailable ? cause : (isUnavailableSend(cause) ? unavailableError(cause, controller.signal) : cause)
|
|
552
579
|
if (cancelled.has(entry.id)) {
|
|
553
580
|
entry.cancelled = true
|
|
554
581
|
if (entry.prepared) { await journal.write(entry); journal.wake() } else { await storage.remove(entry.id); entries.delete(entry.id) }
|
|
555
582
|
} else {
|
|
556
|
-
entry.status = error.retryWhenAvailable ? 'pending' : 'error'
|
|
583
|
+
if (!entry.deliveryConfirmed) entry.status = error.retryWhenAvailable ? 'pending' : 'error'
|
|
584
|
+
entry.failed = true; entry.retryable = retryable(error)
|
|
557
585
|
await journal.write(entry).catch(reportError)
|
|
558
586
|
if (entry.peer !== owner && error.retryWhenAvailable && messenger?.readStatus().paused) pausedSends.add(entry.id)
|
|
559
587
|
else if (isUnavailableSend(cause) && cause.pauseReasons?.length && available && !closed) {
|
|
@@ -566,8 +594,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
566
594
|
// The failed wire event may be a quote or file chunk. Report the
|
|
567
595
|
// owning outbox item separately, without mutating the native error.
|
|
568
596
|
if (error.retryWhenOnline) offlineSends.add(entry.id)
|
|
569
|
-
if (entry.failed && error.retryWhenAvailable && !pausedSends.has(entry.id)) scheduleSendRetry()
|
|
570
|
-
if (!closed && !entry.deletion && !cancelled.has(entry.id) && !error.retryWhenAvailable) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
597
|
+
if (entry.failed && (error.retryWhenAvailable || (entry.deliveryConfirmed && entry.retryable)) && !pausedSends.has(entry.id)) scheduleSendRetry()
|
|
598
|
+
if (!closed && !entry.deletion && !cancelled.has(entry.id) && !entry.deliveryConfirmed && !error.retryWhenAvailable) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
571
599
|
}
|
|
572
600
|
emit()
|
|
573
601
|
} finally { sendControllers.delete(entry.id); releaseForeground() }
|
|
@@ -583,7 +611,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
583
611
|
clearSendRetry()
|
|
584
612
|
sendRetryDelay = SEND_RETRY_MIN_MS
|
|
585
613
|
}
|
|
586
|
-
const pendingRetryable = entry => Boolean(
|
|
614
|
+
const pendingRetryable = entry => Boolean(!cancelled.has(entry.id) && ((entry.localFailed && entry.localRetryable) || (entry.failed && (entry.retryable || entry.status === 'pending'))))
|
|
587
615
|
function scheduleSendRetry () {
|
|
588
616
|
if (closed || !available || sendRetryTimer) return
|
|
589
617
|
const delay = Math.min(SEND_RETRY_MAX_MS, sendRetryDelay * (0.8 + _random() * 0.4))
|
|
@@ -593,13 +621,15 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
593
621
|
if (closed || !available) return
|
|
594
622
|
let retried = false
|
|
595
623
|
for (const entry of entries.values()) {
|
|
596
|
-
if (!pendingRetryable(entry)
|
|
624
|
+
if (!pendingRetryable(entry)) continue
|
|
625
|
+
if (entry.localFailed && entry.localRetryable) { localRetries.add(entry.id); retried = true }
|
|
626
|
+
if (pausedSends.has(entry.id) || !entry.failed || !(entry.retryable || entry.status === 'pending')) continue
|
|
597
627
|
entry.failed = false
|
|
598
628
|
attemptStarts.set(entry.id, performance.now())
|
|
599
629
|
if (entry.status === 'saved') { cleanupEntry(entry); continue }
|
|
600
630
|
retried = true
|
|
601
631
|
}
|
|
602
|
-
if (retried) pump()
|
|
632
|
+
if (retried) { emit(); pump() }
|
|
603
633
|
}, delay)
|
|
604
634
|
sendRetryTimer.unref?.()
|
|
605
635
|
}
|
|
@@ -654,14 +684,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
654
684
|
relocal = false
|
|
655
685
|
for (const entry of entries.values()) {
|
|
656
686
|
if (closed || !available) break
|
|
657
|
-
if (entry.
|
|
658
|
-
if (entry.status === 'saved')
|
|
659
|
-
|
|
660
|
-
await sendEntry(entry, true)
|
|
687
|
+
if ((entry.localFailed && !localRetries.has(entry.id)) || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) continue
|
|
688
|
+
if (localComplete(entry) && !entry.localFailed) { if (entry.status === 'saved') cleanupEntry(entry); continue }
|
|
689
|
+
await saveEntry(entry)
|
|
661
690
|
pumpRemote()
|
|
662
691
|
}
|
|
663
692
|
} while (relocal)
|
|
664
|
-
} finally {
|
|
693
|
+
} finally {
|
|
694
|
+
if ([...entries.values()].some(entry => (entry.localFailed && entry.localRetryable) || (entry.status === 'saved' && entry.failed && entry.retryable))) scheduleSendRetry()
|
|
695
|
+
localSaving = false; for (const resolve of localIdleWaiters) resolve(); localIdleWaiters.clear(); settled()
|
|
696
|
+
}
|
|
665
697
|
}
|
|
666
698
|
async function pumpRemote () {
|
|
667
699
|
if (!available || closed || !storage || !messenger) return
|
|
@@ -673,7 +705,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
673
705
|
for (const entry of entries.values()) {
|
|
674
706
|
if (closed || !available) break
|
|
675
707
|
if (entry.prepared?.accepted) { journal.wake(); continue }
|
|
676
|
-
if (entry.peer === owner || entry.status === 'saved' || !
|
|
708
|
+
if (entry.peer === owner || entry.status === 'saved' || !peers.has(entry.peer) || entry.failed || cancelled.has(entry.id)) continue
|
|
677
709
|
if (!preparation.get(entry.peer) && !preparation.error(entry.peer)) {
|
|
678
710
|
preparation.request(entry.peer).catch(() => {})
|
|
679
711
|
continue
|
|
@@ -803,15 +835,21 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
803
835
|
else clearSendRetry()
|
|
804
836
|
if (changed) lifecycle++
|
|
805
837
|
if (!available) {
|
|
838
|
+
for (const controller of sendControllers.values()) controller.abort()
|
|
806
839
|
journal.setAvailable(false)
|
|
807
840
|
preparation.setAvailable(false)
|
|
808
841
|
for (const peer of channels.keys()) if (!preparation.get(peer)) channels.delete(peer)
|
|
809
|
-
inboxRecovery.stop(); await messenger?.pause('signer')
|
|
842
|
+
inboxRecovery.stop(); await messenger?.pause('signer')
|
|
843
|
+
if (localSaving) await new Promise(resolve => localIdleWaiters.add(resolve))
|
|
844
|
+
return
|
|
810
845
|
}
|
|
811
846
|
await ready()
|
|
812
847
|
journal.setAvailable(true)
|
|
813
848
|
pumpLocal()
|
|
814
|
-
for (const entry of entries.values())
|
|
849
|
+
for (const entry of entries.values()) {
|
|
850
|
+
if (entry.localRetryable) localRetries.add(entry.id)
|
|
851
|
+
if (entry.failed && (entry.retryable || entry.status === 'pending')) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) }
|
|
852
|
+
}
|
|
815
853
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
816
854
|
if (!changed && messenger && !configurationFailed) { preparation.setAvailable(true); pump(); return configuring }
|
|
817
855
|
return schedule()
|
|
@@ -829,7 +867,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
829
867
|
const id = getEventHash(main)
|
|
830
868
|
const events = [...context.map(value => wireEvent(value, owner)), main]
|
|
831
869
|
const files = events.filter(value => value.kind === 1063).map(value => ({ ...decodeFileMetadata(value), optional: !requiredFiles.includes(getEventHash(value)) })).filter(file => file.service === 'irfs').flatMap(file => [file, ...(file.thumbnail?.root ? [{ ...file.thumbnail, optional: file.optional }] : [])])
|
|
832
|
-
const entry = { id, peer, event: { ...main, id }, events, files, index: 0, fileIndex: 0, chunkIndex: 0, localSaved: context.map(() => true).concat(false), status: 'pending', deletion }
|
|
870
|
+
const entry = { id, peer, event: { ...main, id }, events, files, index: 0, fileIndex: 0, chunkIndex: 0, localSaved: context.map(() => true).concat(false), status: 'pending', deliveryConfirmed: false, deletion }
|
|
833
871
|
const startedAt = Date.now()
|
|
834
872
|
debug(entry, 'outbox-start')
|
|
835
873
|
await storage.put(entry)
|
|
@@ -852,6 +890,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
852
890
|
await ready()
|
|
853
891
|
const entry = entries.get(id)
|
|
854
892
|
if (!entry) return
|
|
893
|
+
journal.retry()
|
|
894
|
+
if (entry.deliveryConfirmed) {
|
|
895
|
+
// This action belongs to local recovery, never a fresh network attempt.
|
|
896
|
+
if (entry.localFailed && !entry.localRetryable) return
|
|
897
|
+
if (entry.localFailed) localRetries.add(id)
|
|
898
|
+
entry.failed = false
|
|
899
|
+
resetSendRetry(); emit()
|
|
900
|
+
return pumpLocal()
|
|
901
|
+
}
|
|
902
|
+
if (entry.localFailed) localRetries.add(id)
|
|
855
903
|
if (entry.peer !== owner && entry.status !== 'saved' && available) networkUrgency.set(id, _now() + 30000)
|
|
856
904
|
// Renew while the old wait is still subscribed, preserving the monitor's
|
|
857
905
|
// generation/backoff even if this session is its only consumer.
|
|
@@ -863,16 +911,17 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
863
911
|
},
|
|
864
912
|
async cancel (id) {
|
|
865
913
|
cancelled.add(id)
|
|
914
|
+
localRetries.delete(id)
|
|
866
915
|
sendControllers.get(id)?.abort()
|
|
867
916
|
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
868
917
|
await ready()
|
|
869
918
|
const entry = entries.get(id)
|
|
870
|
-
if (entry?.prepared) { entry.cancelled = true; await journal.write(entry); journal.wake() } else { entries.delete(id);
|
|
919
|
+
if (entry?.prepared) { entry.cancelled = true; await journal.write(entry); journal.wake() } else { entries.delete(id); await storage.remove(id) }
|
|
871
920
|
emit()
|
|
872
921
|
},
|
|
873
922
|
close () {
|
|
874
923
|
return (closeWork ??= (async () => {
|
|
875
|
-
closed = true; available = false; lifecycle
|
|
924
|
+
closed = true; available = false; lifecycle++; localRetries.clear()
|
|
876
925
|
stopOnlineRetry(); offlineSends.clear(); networkUrgency.clear(); activeDownloads.clear(); pendingPriorityRanges.clear()
|
|
877
926
|
clearSendRetry(); inboxRecovery.stop(); preparation.close(); foreground.close(); pausedSends.clear()
|
|
878
927
|
for (const controller of sendControllers.values()) controller.abort()
|