libp2r2p 0.11.25 → 0.11.26
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 +2 -1
- package/network/README.md +31 -0
- package/network/index.js +55 -13
- package/package.json +1 -1
- package/private-messenger/helpers/pause-recovery.js +4 -1
- package/private-messenger/session/index.js +63 -11
package/README.md
CHANGED
|
@@ -10,7 +10,8 @@ power-ups used by that messenger.
|
|
|
10
10
|
|
|
11
11
|
For remote-work scheduling, see [`libp2r2p/network`](network/README.md):
|
|
12
12
|
`isOnline` probes connectivity and `onOnline` shares recovery monitoring,
|
|
13
|
-
including retries when the browser omits its native `online` event
|
|
13
|
+
including retries when the browser omits its native `online` event and optional
|
|
14
|
+
30-second expedited windows for foreground remote sends.
|
|
14
15
|
|
|
15
16
|
## Relay reads and lifecycle
|
|
16
17
|
|
package/network/README.md
CHANGED
|
@@ -30,3 +30,34 @@ Successful probes do not guarantee a particular relay or HTTP server is reachabl
|
|
|
30
30
|
Handle request failures, cancellations, and timeouts separately. Browsers can
|
|
31
31
|
suspend background tabs, so recovery notification has no wall-clock guarantee.
|
|
32
32
|
Consumers should keep their own work queues but should not duplicate probe loops.
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
## Expedited connectivity detection
|
|
36
|
+
|
|
37
|
+
`onOnline(handler, { strict = false, expediteForMs = 0, onDebug })` can request
|
|
38
|
+
an optional urgent window (clamped to 30,000 ms; finite, nonnegative durations).
|
|
39
|
+
The same realm/mode monitor coalesces subscriptions: urgent requests can advance
|
|
40
|
+
the next check, at least two seconds after the previous start. Failed checks
|
|
41
|
+
repeat at five seconds between starts, without overlapping slow checks. Native
|
|
42
|
+
online/focus/visibility wakeups remain available. Removing a subscription removes
|
|
43
|
+
its demand without stopping other listeners.
|
|
44
|
+
|
|
45
|
+
The urgent loop does not spend/reset the ordinary jittered backoff. At expiry it
|
|
46
|
+
returns to the ordinary deadline, or a fresh interval of the preserved retry step
|
|
47
|
+
if that deadline passed; it never catches up missed checks. For example, a normal
|
|
48
|
+
60-second wait remains a 60-second step after an unsuccessful urgent window.
|
|
49
|
+
Connectivity returning after the window must await normal recovery or a new user
|
|
50
|
+
send/retry; automatic retries never extend urgency. `expediteForMs` is scheduling
|
|
51
|
+
advice, not evidence of Internet connectivity or a relay/ACK delivery deadline.
|
|
52
|
+
|
|
53
|
+
Private-message sessions remember a runtime-only 30-second deadline for new
|
|
54
|
+
remote intents and manual retry. Only work actually waiting for network subscribes
|
|
55
|
+
with the remaining time, sharing one subscription per session. Self-chat, restored
|
|
56
|
+
outbox entries, chunks and automatic retries never renew that deadline. Lock and
|
|
57
|
+
close discard urgency. A confirmed monitor result resumes the existing automatic
|
|
58
|
+
pause owner without another immediate probe; explicit pauses remain caller-owned.
|
|
59
|
+
|
|
60
|
+
Optional `onDebug` reports only probe reason, state, remaining window and duration.
|
|
61
|
+
Consumers wire it only in development; it is disabled by default. The monitor
|
|
62
|
+
retains no diagnostic history. Session diagnostics additionally correlate the
|
|
63
|
+
network wait and pause-release duration with the event ID.
|
package/network/index.js
CHANGED
|
@@ -133,6 +133,8 @@ export function createConnectivityMonitor ({
|
|
|
133
133
|
_setTimeout = globalThis.setTimeout,
|
|
134
134
|
_clearTimeout = globalThis.clearTimeout,
|
|
135
135
|
_random = Math.random,
|
|
136
|
+
_now = () => performance.now(),
|
|
137
|
+
onDebug,
|
|
136
138
|
reportError = error => console.error('Online listener failed', error)
|
|
137
139
|
} = {}) {
|
|
138
140
|
if (check !== undefined && typeof check !== 'function') throw new ValidationError('INVALID_CONNECTIVITY_CHECK')
|
|
@@ -148,21 +150,49 @@ export function createConnectivityMonitor ({
|
|
|
148
150
|
try { Promise.resolve(entry.handler()).catch(reportError) } catch (error) { reportError(error) }
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
const urgencyEnd = () => Math.max(0, ...[...listeners].map(entry => entry.expediteUntil))
|
|
154
|
+
function arm (current) {
|
|
155
|
+
if (session !== current || current.pending || !listeners.size) return
|
|
156
|
+
if (current.timer != null) clearTimer(current.timer)
|
|
157
|
+
const now = _now()
|
|
158
|
+
const until = urgencyEnd()
|
|
159
|
+
const urgent = until > now && (!current.online || current.urgentDue != null)
|
|
160
|
+
// Urgent checks do not consume or reset the ordinary retry sequence.
|
|
161
|
+
if (!urgent && current.normalDue <= now) current.normalDue = now + current.normalDelay
|
|
162
|
+
let due = current.normalDue
|
|
163
|
+
if (urgent) due = Math.min(until, current.urgentDue ?? current.lastStartedAt + 5000)
|
|
154
164
|
current.timer = setTimer(() => {
|
|
155
165
|
current.timer = null
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
if (urgent && _now() >= urgencyEnd()) { arm(current); return }
|
|
167
|
+
return probe(current, urgent ? 'expedited' : 'backoff')
|
|
168
|
+
}, Math.max(0, due - now))
|
|
158
169
|
current.timer?.unref?.()
|
|
159
170
|
}
|
|
160
171
|
|
|
161
|
-
|
|
172
|
+
function schedule (current, expedited) {
|
|
173
|
+
if (session !== current || !listeners.size) return
|
|
174
|
+
if (!expedited || current.online) {
|
|
175
|
+
const delay = current.online ? 60000 : RETRY_DELAYS[Math.min(current.retry++, RETRY_DELAYS.length - 1)]
|
|
176
|
+
current.normalDelay = Math.round(delay * (0.8 + _random() * 0.4))
|
|
177
|
+
current.normalDue = _now() + current.normalDelay
|
|
178
|
+
}
|
|
179
|
+
current.urgentDue = null
|
|
180
|
+
arm(current)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function probe (current, reason = 'wake') {
|
|
162
184
|
if (session !== current || current.pending) return
|
|
163
185
|
if (current.timer != null) clearTimer(current.timer)
|
|
164
186
|
current.timer = null
|
|
165
187
|
current.pending = true
|
|
188
|
+
current.lastStartedAt = _now()
|
|
189
|
+
const expedited = reason === 'expedited'
|
|
190
|
+
const startedAt = _now()
|
|
191
|
+
const report = values => {
|
|
192
|
+
const entry = { stage: 'connectivity-probe', reason, expediteRemainingMs: Math.max(0, urgencyEnd() - _now()), ...values }
|
|
193
|
+
for (const callback of [onDebug, ...[...listeners].map(listener => listener.onDebug)]) { try { callback?.(entry) } catch {} }
|
|
194
|
+
}
|
|
195
|
+
report({ state: 'started' })
|
|
166
196
|
try {
|
|
167
197
|
const online = await runCheck({ signal: current.controller.signal })
|
|
168
198
|
if (session !== current) return
|
|
@@ -179,12 +209,13 @@ export function createConnectivityMonitor ({
|
|
|
179
209
|
for (const entry of listeners) entry.delivered = false
|
|
180
210
|
} finally {
|
|
181
211
|
current.pending = false
|
|
182
|
-
|
|
212
|
+
report({ state: current.online ? 'online' : 'offline', elapsedMs: _now() - startedAt })
|
|
213
|
+
schedule(current, expedited)
|
|
183
214
|
}
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
function start () {
|
|
187
|
-
const current = { online: false, retry: 0, timer: null, pending: false, controller: new AbortController() }
|
|
218
|
+
const current = { online: false, retry: 0, timer: null, pending: false, lastStartedAt: -Infinity, normalDue: Infinity, normalDelay: 5000, urgentDue: null, controller: new AbortController() }
|
|
188
219
|
session = current
|
|
189
220
|
current.wake = () => { probe(current) }
|
|
190
221
|
current.connectionChanged = () => {
|
|
@@ -213,18 +244,29 @@ export function createConnectivityMonitor ({
|
|
|
213
244
|
}
|
|
214
245
|
|
|
215
246
|
// Notify once on confirmed initial connectivity, then after each detected reconnection.
|
|
216
|
-
function onOnline (handler) {
|
|
247
|
+
function onOnline (handler, { expediteForMs = 0, onDebug } = {}) {
|
|
217
248
|
if (typeof handler !== 'function') throw new ValidationError('INVALID_ONLINE_HANDLER')
|
|
218
|
-
|
|
249
|
+
if (!Number.isFinite(expediteForMs) || expediteForMs < 0) throw new ValidationError('INVALID_EXPEDITE_FOR_MS')
|
|
250
|
+
if (onDebug !== undefined && typeof onDebug !== 'function') throw new ValidationError('INVALID_CONNECTIVITY_DEBUG')
|
|
251
|
+
const entry = { handler, onDebug, delivered: false, expediteUntil: _now() + Math.min(expediteForMs, 30000) }
|
|
219
252
|
listeners.add(entry)
|
|
220
253
|
if (!session) start()
|
|
221
254
|
else {
|
|
222
255
|
const current = session
|
|
223
|
-
|
|
256
|
+
if (expediteForMs > 0 || urgencyEnd() > _now()) {
|
|
257
|
+
current.urgentDue = Math.max(_now(), current.lastStartedAt + 2000)
|
|
258
|
+
arm(current)
|
|
259
|
+
} else {
|
|
260
|
+
queueMicrotask(() => {
|
|
261
|
+
if (urgencyEnd() > _now()) arm(current)
|
|
262
|
+
else probe(current)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
224
265
|
}
|
|
225
266
|
return () => {
|
|
226
267
|
if (!listeners.delete(entry)) return
|
|
227
268
|
if (!listeners.size) stop()
|
|
269
|
+
else arm(session)
|
|
228
270
|
}
|
|
229
271
|
}
|
|
230
272
|
|
|
@@ -234,12 +276,12 @@ export function createConnectivityMonitor ({
|
|
|
234
276
|
const defaultMonitors = new Map()
|
|
235
277
|
|
|
236
278
|
// Share probes, capped backoff and wake-up listeners per mode in this realm.
|
|
237
|
-
export function onOnline (handler, { strict = false } = {}) {
|
|
279
|
+
export function onOnline (handler, { strict = false, expediteForMs = 0, onDebug } = {}) {
|
|
238
280
|
const key = strict ? 'strict' : 'lenient'
|
|
239
281
|
let monitor = defaultMonitors.get(key)
|
|
240
282
|
if (!monitor) {
|
|
241
283
|
monitor = createConnectivityMonitor({ strict })
|
|
242
284
|
defaultMonitors.set(key, monitor)
|
|
243
285
|
}
|
|
244
|
-
return monitor.onOnline(handler)
|
|
286
|
+
return monitor.onOnline(handler, { expediteForMs, onDebug })
|
|
245
287
|
}
|
package/package.json
CHANGED
|
@@ -33,11 +33,14 @@ export function createPauseRecovery ({ attempt, online, onOnline, retryable, onE
|
|
|
33
33
|
const run = () => {
|
|
34
34
|
if (!active() || job.work) return job.work
|
|
35
35
|
const work = (async () => {
|
|
36
|
-
|
|
36
|
+
const confirmedOnline = job.confirmedOnline
|
|
37
|
+
job.confirmedOnline = false
|
|
38
|
+
if (online && !confirmedOnline && !await online({ signal: job.controller.signal })) {
|
|
37
39
|
if (!active()) return
|
|
38
40
|
job.stopOnline = onOnline(() => {
|
|
39
41
|
if (!active()) return
|
|
40
42
|
job.stopOnline?.(); job.stopOnline = null
|
|
43
|
+
job.confirmedOnline = true
|
|
41
44
|
schedule(0)
|
|
42
45
|
})
|
|
43
46
|
return
|
|
@@ -45,7 +45,7 @@ function messengerStatusSnapshot (value) {
|
|
|
45
45
|
return { closed: value.closed, paused: value.paused, pauseReasons: [...value.pauseReasons] }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, pendingDecryptionReady = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, onDebug, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
48
|
+
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, pendingDecryptionReady = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, onDebug, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, _now = () => performance.now(), openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
49
49
|
fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
50
50
|
fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
|
|
51
51
|
if (typeof pendingDecryptionReady !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
@@ -67,6 +67,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
67
67
|
const entries = new Map()
|
|
68
68
|
const cancelled = new Set()
|
|
69
69
|
const attemptStarts = new Map()
|
|
70
|
+
const networkUrgency = new Map()
|
|
70
71
|
const sendControllers = new Map()
|
|
71
72
|
const localReady = new Set()
|
|
72
73
|
const localIdleWaiters = new Set()
|
|
@@ -79,6 +80,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
79
80
|
const offlineSends = new Set()
|
|
80
81
|
const pausedSends = new Set()
|
|
81
82
|
let messengerStatus
|
|
83
|
+
let networkPauseStartedAt
|
|
84
|
+
let networkConfirmed = false
|
|
82
85
|
let messengerBinding
|
|
83
86
|
let storageBlocked = false
|
|
84
87
|
const inboxRecovery = createPauseRecovery({
|
|
@@ -89,22 +92,31 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
89
92
|
let inboxError
|
|
90
93
|
const stateChanged = state => {
|
|
91
94
|
if (closed) return
|
|
95
|
+
const wasNetworkPaused = messengerStatus?.pauseReasons.includes('network')
|
|
92
96
|
messengerStatus = messengerStatusSnapshot(state)
|
|
97
|
+
if (!wasNetworkPaused && state.pauseReasons.includes('network')) { networkPauseStartedAt = _now(); networkConfirmed = false }
|
|
98
|
+
if (wasNetworkPaused && !state.pauseReasons.includes('network')) {
|
|
99
|
+
for (const entry of entries.values()) if (entry.peer !== owner && entry.status !== 'saved') debug(entry, 'network-pause-released', undefined, { elapsedMs: _now() - networkPauseStartedAt })
|
|
100
|
+
networkPauseStartedAt = undefined; networkConfirmed = false
|
|
101
|
+
}
|
|
93
102
|
if (!available || state.closed) return
|
|
94
103
|
if (state.paused) {
|
|
95
104
|
for (const entry of entries.values()) {
|
|
96
105
|
if (entry.peer !== owner && entry.failed && entry.status === 'pending') pausedSends.add(entry.id)
|
|
97
106
|
}
|
|
98
107
|
if (![...entries.values()].some(entry => pendingRetryable(entry) && !pausedSends.has(entry.id))) clearSendRetry()
|
|
108
|
+
watchOfflineSends()
|
|
99
109
|
return
|
|
100
110
|
}
|
|
101
111
|
clearSendRetry()
|
|
102
112
|
for (const id of pausedSends) { const entry = entries.get(id); if (entry) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) } }
|
|
103
113
|
pausedSends.clear()
|
|
114
|
+
watchOfflineSends()
|
|
104
115
|
// The callback can run inside construction/resume, before configure settles.
|
|
105
116
|
queueMicrotask(() => pump())
|
|
106
117
|
}
|
|
107
118
|
let stopOnline
|
|
119
|
+
let requestedUrgency = 0
|
|
108
120
|
let sendRetryTimer
|
|
109
121
|
let sendRetryDelay = SEND_RETRY_MIN_MS
|
|
110
122
|
let messenger
|
|
@@ -152,6 +164,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
152
164
|
}
|
|
153
165
|
},
|
|
154
166
|
changed: entry => {
|
|
167
|
+
watchOfflineSends()
|
|
155
168
|
if (!entry.prepared && (entry.status === 'saved' || entry.cancelled)) cleanupEntry(entry)
|
|
156
169
|
else if (!closed && available) pumpRemote()
|
|
157
170
|
}
|
|
@@ -559,7 +572,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
559
572
|
emit()
|
|
560
573
|
} finally { sendControllers.delete(entry.id); releaseForeground() }
|
|
561
574
|
}
|
|
562
|
-
function stopOnlineRetry () { stopOnline?.(); stopOnline = undefined }
|
|
575
|
+
function stopOnlineRetry () { stopOnline?.(); stopOnline = undefined; requestedUrgency = 0 }
|
|
563
576
|
// Transient availability failures without an active messenger pause retain
|
|
564
577
|
// bounded retries. Observed pauses wait exclusively for their release.
|
|
565
578
|
function clearSendRetry () {
|
|
@@ -591,21 +604,47 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
591
604
|
sendRetryTimer.unref?.()
|
|
592
605
|
}
|
|
593
606
|
function watchOfflineSends () {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
607
|
+
const waiting = [...entries.values()].filter(entry => entry.peer !== owner && peers.has(entry.peer) && entry.status !== 'saved' && !entry.cancelled && !cancelled.has(entry.id) && !entry.prepared?.accepted && (!entry.failed || pendingRetryable(entry)) &&
|
|
608
|
+
(offlineSends.has(entry.id) || (!networkConfirmed && messengerStatus?.pauseReasons.includes('network'))))
|
|
609
|
+
for (const id of networkUrgency.keys()) {
|
|
610
|
+
const entry = entries.get(id)
|
|
611
|
+
if (!entry || entry.status === 'saved' || entry.cancelled || cancelled.has(id)) networkUrgency.delete(id)
|
|
612
|
+
}
|
|
613
|
+
if (closed || !available || !waiting.length) { stopOnlineRetry(); return }
|
|
614
|
+
const deadline = Math.max(0, ...waiting.map(entry => networkUrgency.get(entry.id) ?? 0))
|
|
615
|
+
if (stopOnline && deadline === requestedUrgency) return
|
|
616
|
+
const previous = stopOnline
|
|
617
|
+
requestedUrgency = deadline
|
|
618
|
+
const startedAt = _now()
|
|
619
|
+
// Replacing a subscription retains the shared loop and its backoff. This is
|
|
620
|
+
// only done when a new user action extends the window, never per chunk/retry.
|
|
598
621
|
stopOnline = _onOnline(() => {
|
|
599
622
|
stopOnlineRetry()
|
|
600
623
|
if (closed || !available) return
|
|
624
|
+
networkConfirmed = true
|
|
625
|
+
for (const entry of waiting) debug(entry, 'network-confirmed', undefined, { elapsedMs: _now() - startedAt })
|
|
601
626
|
for (const id of offlineSends) {
|
|
602
627
|
const entry = entries.get(id)
|
|
603
628
|
if (entry?.retryable) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) }
|
|
604
629
|
}
|
|
605
630
|
offlineSends.clear()
|
|
631
|
+
// The messenger's automatic recovery owns its network pause; the same
|
|
632
|
+
// monitor also notifies that recovery job. Never resume explicit pauses.
|
|
606
633
|
return schedule()
|
|
634
|
+
}, {
|
|
635
|
+
expediteForMs: Math.max(0, deadline - _now()),
|
|
636
|
+
...(onDebug
|
|
637
|
+
? {
|
|
638
|
+
onDebug: measure => {
|
|
639
|
+
const entry = waiting[0]
|
|
640
|
+
debug(entry, measure.stage, undefined, measure)
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
: {})
|
|
607
644
|
})
|
|
645
|
+
previous?.()
|
|
608
646
|
}
|
|
647
|
+
|
|
609
648
|
async function pumpLocal () {
|
|
610
649
|
if (!available || closed || !storage) return
|
|
611
650
|
if (localSaving) { relocal = true; return }
|
|
@@ -738,6 +777,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
738
777
|
const next = new Set([...values].filter(peer => peer !== owner))
|
|
739
778
|
if (next.size === peers.size && [...next].every(peer => peers.has(peer))) return configuring
|
|
740
779
|
peers.clear(); for (const peer of next) peers.add(peer)
|
|
780
|
+
watchOfflineSends()
|
|
741
781
|
preparation.reconcile(peers)
|
|
742
782
|
if (pendingDecryptionPeer && !peers.has(pendingDecryptionPeer)) {
|
|
743
783
|
pendingDecryptionPeer = null
|
|
@@ -757,7 +797,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
757
797
|
const changed = available !== (value === true)
|
|
758
798
|
available = value === true
|
|
759
799
|
if (!changed && !available) return
|
|
760
|
-
stopOnlineRetry(); offlineSends.clear()
|
|
800
|
+
if (changed || !available) { stopOnlineRetry(); offlineSends.clear() }
|
|
801
|
+
if (!available) { networkUrgency.clear(); networkConfirmed = false }
|
|
761
802
|
if (available) resetSendRetry()
|
|
762
803
|
else clearSendRetry()
|
|
763
804
|
if (changed) lifecycle++
|
|
@@ -778,6 +819,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
778
819
|
async enqueue ({ peer, event, context = [], requiredFiles = [], deletion = false, attemptStartedAt }) {
|
|
779
820
|
if (closed || !available || (peer !== owner && !peers.has(peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
780
821
|
if (attemptStartedAt !== undefined && (!Number.isFinite(attemptStartedAt) || attemptStartedAt < 0)) throw new ValidationError('INVALID_ATTEMPT_STARTED_AT')
|
|
822
|
+
const urgencyUntil = _now() + 30000
|
|
823
|
+
const urgencyLifecycle = lifecycle
|
|
781
824
|
const releaseForeground = foreground.enter()
|
|
782
825
|
enqueuing++
|
|
783
826
|
try {
|
|
@@ -797,17 +840,26 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
797
840
|
// An accepted durable intent survives shutdown/locking for replay. Only
|
|
798
841
|
// explicit cancellation removes it; no late callback or work is admitted.
|
|
799
842
|
if (closed) return id
|
|
800
|
-
entries.set(id, entry)
|
|
843
|
+
entries.set(id, entry)
|
|
844
|
+
if (peer !== owner && available && lifecycle === urgencyLifecycle) networkUrgency.set(id, urgencyUntil)
|
|
845
|
+
watchOfflineSends(); emit(); pump()
|
|
801
846
|
if (available && peer !== owner) preparePeer(peer).catch(() => {})
|
|
802
847
|
return id
|
|
803
848
|
} finally { releaseForeground(); enqueuing--; settled() }
|
|
804
849
|
},
|
|
805
850
|
async retry (id) {
|
|
806
|
-
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
807
851
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
808
852
|
await ready()
|
|
809
853
|
const entry = entries.get(id)
|
|
810
|
-
if (entry)
|
|
854
|
+
if (!entry) return
|
|
855
|
+
if (entry.peer !== owner && entry.status !== 'saved' && available) networkUrgency.set(id, _now() + 30000)
|
|
856
|
+
// Renew while the old wait is still subscribed, preserving the monitor's
|
|
857
|
+
// generation/backoff even if this session is its only consumer.
|
|
858
|
+
watchOfflineSends()
|
|
859
|
+
offlineSends.delete(id); pausedSends.delete(id)
|
|
860
|
+
attemptStarts.set(id, performance.now()); journal.retry(); entry.failed = false; resetSendRetry()
|
|
861
|
+
if (entry.peer !== owner) preparePeer(entry.peer).catch(() => {})
|
|
862
|
+
return pump()
|
|
811
863
|
},
|
|
812
864
|
async cancel (id) {
|
|
813
865
|
cancelled.add(id)
|
|
@@ -821,7 +873,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
821
873
|
close () {
|
|
822
874
|
return (closeWork ??= (async () => {
|
|
823
875
|
closed = true; available = false; lifecycle++
|
|
824
|
-
stopOnlineRetry(); offlineSends.clear(); activeDownloads.clear(); pendingPriorityRanges.clear()
|
|
876
|
+
stopOnlineRetry(); offlineSends.clear(); networkUrgency.clear(); activeDownloads.clear(); pendingPriorityRanges.clear()
|
|
825
877
|
clearSendRetry(); inboxRecovery.stop(); preparation.close(); foreground.close(); pausedSends.clear()
|
|
826
878
|
for (const controller of sendControllers.values()) controller.abort()
|
|
827
879
|
await Promise.allSettled([configuring, initialized, messengerWork, fileSetupWork])
|