libp2r2p 0.11.12 → 0.11.13
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
|
@@ -991,7 +991,10 @@ messages, history and files while preserving automatic NIP-65 updates. Successfu
|
|
|
991
991
|
latency, and short-lived channel/recipient preferences avoid recently refused
|
|
992
992
|
relays without changing receive subscriptions. See the
|
|
993
993
|
[session coordinator](private-messenger/session/README.md) for deferred offline
|
|
994
|
-
attempts and final failure notifications.
|
|
994
|
+
attempts and final failure notifications. Sends attempted while the messenger is
|
|
995
|
+
paused or otherwise unavailable now stay pending in the outbox and retry on a
|
|
996
|
+
bounded session backoff (1–30 seconds, 20% jitter) instead of surfacing a
|
|
997
|
+
user-facing send error.
|
|
995
998
|
|
|
996
999
|
|
|
997
1000
|
### Relay backpressure and bounded synchronization (0.11.7)
|
|
@@ -1030,10 +1033,12 @@ readiness on the replacement. Callbacks and completions are generation-fenced.
|
|
|
1030
1033
|
Equal watch settings cannot keep a dead subscription registered as active.
|
|
1031
1034
|
|
|
1032
1035
|
Overflow recovery keeps one timer per relay with exponential 1–30 second
|
|
1033
|
-
backoff and 20% jitter, reset after a stable minute.
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1036
|
+
backoff and 20% jitter, reset after a stable minute. Relay-pool capacity closes
|
|
1037
|
+
(1013, "relay pool capacity") use the same relay backoff instead of stopping
|
|
1038
|
+
until an explicit rebuild, because the launcher pool bucket budget is transient.
|
|
1039
|
+
Other relays remain live. Removal, pause and close cancel pending work. Fragment
|
|
1040
|
+
storage stays scoped to the same session across replacement subscriptions.
|
|
1041
|
+
Permanent refusals remain reported and do not trigger this automatic retry loop.
|
|
1037
1042
|
|
|
1038
1043
|
PrivateMessenger persists each interrupted interval before reopening, then
|
|
1039
1044
|
uses paged `fetchHistory` to repair the gap while listening for new messages.
|
package/package.json
CHANGED
package/private-message/index.js
CHANGED
|
@@ -21,6 +21,18 @@ function uniq (values) {
|
|
|
21
21
|
return [...new Set((values || []).filter(Boolean))]
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
// The launcher's shared relay pool closes anonymous members with 1013 when its
|
|
25
|
+
// per-relay buckets are all busy. That is a transient pool condition, not a
|
|
26
|
+
// permanent relay rejection, so the shared subscription must keep the normal
|
|
27
|
+
// relay backoff instead of stopping until something else re-opens it.
|
|
28
|
+
function isRelayPoolCapacity (error) {
|
|
29
|
+
for (let current = error, depth = 0; current && depth < 3; current = current.cause, depth++) {
|
|
30
|
+
if (current.closeCode === 1013 || current.code === 'RELAY_POOL_CAPACITY') return true
|
|
31
|
+
if (/relay pool capacity/i.test(current.message || '')) return true
|
|
32
|
+
}
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
function normalizeDeletionPubkey (deletionPubkey) {
|
|
25
37
|
if (deletionPubkey === undefined) return undefined
|
|
26
38
|
if (typeof deletionPubkey !== 'string' || !HEX_PUBKEY.test(deletionPubkey)) {
|
|
@@ -360,8 +372,9 @@ export function createPrivateMessageSession ({ _setTimeout = setTimeout, _clearT
|
|
|
360
372
|
delay: entry.readyAt !== undefined && _now() - entry.readyAt >= 60000 ? 1000 : (previous?.delay || entry.retryDelay || 1000),
|
|
361
373
|
notified: new Map(), timer: null
|
|
362
374
|
}
|
|
363
|
-
|
|
364
|
-
|
|
375
|
+
const permanent = /^(auth-required:|restricted:|blocked:|invalid:|pow:)/.test(error.message || '') || error.name === 'ValidationError'
|
|
376
|
+
const retryable = error.code === 'RELAY_LIVE_BUFFER_FULL' || isRelayPoolCapacity(error) || Boolean(previous)
|
|
377
|
+
if (permanent || !retryable) {
|
|
365
378
|
cancelRelayRecovery(relay)
|
|
366
379
|
notifyInterruption(relay, recovery).catch(error => reportSubscriptionError(entry.channels, error))
|
|
367
380
|
return
|
|
@@ -102,6 +102,11 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
102
102
|
errors: reports.flatMap(report => report.errors || [])
|
|
103
103
|
}))
|
|
104
104
|
})
|
|
105
|
+
// A lifecycle change (signer/pause) during a batch is not a user abort:
|
|
106
|
+
// every failure summary stays retryable so the session can re-attempt it.
|
|
107
|
+
const finished = result => current()
|
|
108
|
+
? finish(result)
|
|
109
|
+
: { ...finish(result), retryWhenAvailable: !signal?.aborted, retryWhenOnline: !signal?.aborted }
|
|
105
110
|
let result = { success: false }
|
|
106
111
|
while ((batch ||= next())) {
|
|
107
112
|
signal?.throwIfAborted()
|
|
@@ -124,15 +129,15 @@ export function createSendRelayRouting ({ peer, peers = [peer], relaysByPubkey,
|
|
|
124
129
|
const report = await settled
|
|
125
130
|
const failed = rejected(report)
|
|
126
131
|
errors.push(...(report?.errors || []))
|
|
127
|
-
if (!failed.length || errors.some(item => !isReplaceableRelayFailure(item.reason))) return
|
|
128
|
-
if (!next()) { exclusions.clear(); return
|
|
129
|
-
if (!await online()) return { ...finish(result), retryWhenAvailable: !signal?.aborted, retryWhenOnline:
|
|
132
|
+
if (!failed.length || errors.some(item => !isReplaceableRelayFailure(item.reason))) return finished(result)
|
|
133
|
+
if (!next()) { exclusions.clear(); return finished(result) }
|
|
134
|
+
if (!await online()) return { ...finish(result), retryWhenAvailable: !signal?.aborted, retryWhenOnline: !signal?.aborted }
|
|
130
135
|
remember(failed)
|
|
131
136
|
}
|
|
132
|
-
if (!current()) return
|
|
137
|
+
if (!current()) return finished(result)
|
|
133
138
|
batch = null
|
|
134
139
|
}
|
|
135
|
-
return
|
|
140
|
+
return finished(result)
|
|
136
141
|
}
|
|
137
142
|
|
|
138
143
|
return {
|
|
@@ -40,6 +40,14 @@ or cancelled/closed sends. Apps own route visibility and user-intent filtering;
|
|
|
40
40
|
automatic retries may also fail. Callbacks must not throw. Errors are not copied
|
|
41
41
|
into persisted outbox records.
|
|
42
42
|
|
|
43
|
+
Sends that only failed because the messenger was paused or otherwise
|
|
44
|
+
unavailable (`PRIVATE_MESSENGER_PAUSED`, `CHAT_UNAVAILABLE`) stay pending
|
|
45
|
+
without `onSendError`. The session re-pumps them on a bounded backoff
|
|
46
|
+
(1–30 seconds with 20% jitter) in addition to availability/connectivity
|
|
47
|
+
changes, so a paused messenger recovers without a manual retry. The backoff
|
|
48
|
+
resets on a user `retry`, on `setAvailable(true)`, and once no retryable entry
|
|
49
|
+
remains.
|
|
50
|
+
|
|
43
51
|
`enqueue({ peer, event, context, requiredFiles, deletion })` preserves prepared
|
|
44
52
|
identities. Local message commits and remote stages have independent checkpoints.
|
|
45
53
|
It sends original/thumbnail chunks before announcements, yields for small messages,
|
|
@@ -18,8 +18,20 @@ export function wireEvent (value, owner) {
|
|
|
18
18
|
}
|
|
19
19
|
// Relay rejection text must not be interpreted as a local signer denial.
|
|
20
20
|
const retryable = error => error?.code === 'MESSAGE_NOT_PUBLISHED' || !/DENIED|PERMISSION|REVOKED|READ_ONLY|INVALID|BLOCKED|EXPIRED|NOT_IN_PERSONA/i.test(`${error?.code || ''} ${error?.message || ''}`)
|
|
21
|
+
const unavailableSend = /PRIVATE_MESSENGER_PAUSED|CHAT_UNAVAILABLE|MESSENGER_UNAVAILABLE/i
|
|
22
|
+
const isUnavailableSend = error => unavailableSend.test(`${error?.code || ''} ${error?.message || ''}`)
|
|
23
|
+
const SEND_RETRY_MIN_MS = 1000
|
|
24
|
+
const SEND_RETRY_MAX_MS = 30000
|
|
25
|
+
// A paused or unavailable messenger must keep the outbox entry retryable
|
|
26
|
+
// instead of surfacing a send failure the user has to retry by hand.
|
|
27
|
+
const unavailableError = (error, signal) => Object.assign(new Error(error?.message || 'CHAT_UNAVAILABLE'), {
|
|
28
|
+
code: error?.code,
|
|
29
|
+
cause: error,
|
|
30
|
+
retryWhenAvailable: !signal?.aborted,
|
|
31
|
+
retryWhenOnline: false
|
|
32
|
+
})
|
|
21
33
|
|
|
22
|
-
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _getIykcProofs = getIykcProofs, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
34
|
+
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
23
35
|
fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
24
36
|
const reportedErrors = new WeakSet()
|
|
25
37
|
const reportError = error => {
|
|
@@ -40,6 +52,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
40
52
|
const sendControllers = new Map()
|
|
41
53
|
const offlineSends = new Set()
|
|
42
54
|
let stopOnline
|
|
55
|
+
let sendRetryTimer
|
|
56
|
+
let sendRetryDelay = SEND_RETRY_MIN_MS
|
|
43
57
|
let messenger
|
|
44
58
|
let fileTransfers
|
|
45
59
|
let downloads
|
|
@@ -60,6 +74,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
60
74
|
let lifecycle = 0
|
|
61
75
|
let configuring = Promise.resolve()
|
|
62
76
|
let sending
|
|
77
|
+
let repump
|
|
63
78
|
let draining
|
|
64
79
|
const idleWaiters = new Set()
|
|
65
80
|
const settled = () => { if (!sending && !draining) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
@@ -179,9 +194,15 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
179
194
|
}
|
|
180
195
|
async function publish (peer, event, signal) {
|
|
181
196
|
if (deniedPeers.has(peer)) throw new Error('PERMISSION_DENIED')
|
|
182
|
-
if (!available || !peers.has(peer) || !channels.has(peer)) throw new Error('CHAT_UNAVAILABLE')
|
|
197
|
+
if (!available || !peers.has(peer) || !channels.has(peer)) throw unavailableError(new Error('CHAT_UNAVAILABLE'), signal)
|
|
183
198
|
const options = { channelPubkey: channels.get(peer)?.pubkey, receiverPubkeys: [peer], signal }
|
|
184
|
-
|
|
199
|
+
let report
|
|
200
|
+
try {
|
|
201
|
+
report = event.sig ? await messenger.broadcastEvent({ ...options, event }) : await messenger.broadcastRumor({ ...options, rumor: event })
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (isUnavailableSend(error)) throw unavailableError(error, signal)
|
|
204
|
+
throw error
|
|
205
|
+
}
|
|
185
206
|
// Published is relay acceptance, never a peer receipt.
|
|
186
207
|
await assertMessagePublished(report, event)
|
|
187
208
|
}
|
|
@@ -245,7 +266,10 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
245
266
|
await storage.remove(entry.id)
|
|
246
267
|
entries.delete(entry.id)
|
|
247
268
|
emit()
|
|
248
|
-
} catch (
|
|
269
|
+
} catch (cause) {
|
|
270
|
+
// Pauses can surface at any stage (text publication, file authorization
|
|
271
|
+
// or chunk delivery). Normalize all of them into the retryable path.
|
|
272
|
+
const error = cause?.retryWhenAvailable ? cause : (isUnavailableSend(cause) ? unavailableError(cause, controller.signal) : cause)
|
|
249
273
|
if (cancelled.has(entry.id)) { await storage.remove(entry.id); entries.delete(entry.id) } else {
|
|
250
274
|
entry.status = error.retryWhenAvailable ? 'pending' : 'error'; entry.failed = true; entry.retryable = retryable(error)
|
|
251
275
|
await storage.put(entry, { existing: true }).catch(reportError)
|
|
@@ -253,12 +277,42 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
253
277
|
// The failed wire event may be a quote or file chunk. Report the
|
|
254
278
|
// owning outbox item separately, without mutating the native error.
|
|
255
279
|
if (error.retryWhenOnline) offlineSends.add(entry.id)
|
|
280
|
+
if (error.retryWhenAvailable) scheduleSendRetry()
|
|
256
281
|
if (!closed && !entry.deletion && !cancelled.has(entry.id) && !error.retryWhenAvailable) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
257
282
|
}
|
|
258
283
|
emit()
|
|
259
284
|
} finally { sendControllers.delete(entry.id) }
|
|
260
285
|
}
|
|
261
286
|
function stopOnlineRetry () { stopOnline?.(); stopOnline = undefined }
|
|
287
|
+
// Entries that failed only because the messenger was paused/unavailable do
|
|
288
|
+
// not receive a connectivity transition, so they own a bounded backoff timer
|
|
289
|
+
// instead of waiting for the next account-state change.
|
|
290
|
+
function clearSendRetry () {
|
|
291
|
+
if (sendRetryTimer) _clearTimeout(sendRetryTimer)
|
|
292
|
+
sendRetryTimer = undefined
|
|
293
|
+
}
|
|
294
|
+
function resetSendRetry () {
|
|
295
|
+
clearSendRetry()
|
|
296
|
+
sendRetryDelay = SEND_RETRY_MIN_MS
|
|
297
|
+
}
|
|
298
|
+
const pendingRetryable = entry => Boolean(entry.failed && !cancelled.has(entry.id) && (entry.retryable || entry.status === 'pending'))
|
|
299
|
+
function scheduleSendRetry () {
|
|
300
|
+
if (closed || !available || sendRetryTimer) return
|
|
301
|
+
const delay = Math.min(SEND_RETRY_MAX_MS, sendRetryDelay * (0.8 + _random() * 0.4))
|
|
302
|
+
sendRetryDelay = Math.min(SEND_RETRY_MAX_MS, sendRetryDelay * 2)
|
|
303
|
+
sendRetryTimer = _setTimeout(() => {
|
|
304
|
+
sendRetryTimer = undefined
|
|
305
|
+
if (closed || !available) return
|
|
306
|
+
let retried = false
|
|
307
|
+
for (const entry of entries.values()) {
|
|
308
|
+
if (!pendingRetryable(entry)) continue
|
|
309
|
+
entry.failed = false
|
|
310
|
+
retried = true
|
|
311
|
+
}
|
|
312
|
+
if (retried) pump()
|
|
313
|
+
}, delay)
|
|
314
|
+
sendRetryTimer.unref?.()
|
|
315
|
+
}
|
|
262
316
|
function watchOfflineSends () {
|
|
263
317
|
if (closed || !available || !offlineSends.size) { stopOnlineRetry(); return }
|
|
264
318
|
if (stopOnline) return
|
|
@@ -276,15 +330,26 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
276
330
|
})
|
|
277
331
|
}
|
|
278
332
|
async function pump () {
|
|
279
|
-
if (
|
|
333
|
+
if (!available || closed || !messenger || !storage) return
|
|
334
|
+
// A retry that lands while a pump is already running must not be skipped
|
|
335
|
+
// just because that pass already walked past the entry.
|
|
336
|
+
if (sending) { repump = true; return }
|
|
280
337
|
sending = true
|
|
281
338
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
339
|
+
do {
|
|
340
|
+
repump = false
|
|
341
|
+
for (const entry of entries.values()) {
|
|
342
|
+
if (closed || !available) break
|
|
343
|
+
if ((entry.peer !== owner && !peers.has(entry.peer)) || entry.failed || cancelled.has(entry.id)) continue
|
|
344
|
+
await sendEntry(entry)
|
|
345
|
+
}
|
|
346
|
+
} while (repump && !closed && available)
|
|
347
|
+
} finally {
|
|
348
|
+
sending = false
|
|
349
|
+
watchOfflineSends()
|
|
350
|
+
if (![...entries.values()].some(pendingRetryable)) resetSendRetry()
|
|
351
|
+
settled()
|
|
352
|
+
}
|
|
288
353
|
}
|
|
289
354
|
const intentId = file => getEventHash({ kind: 0, pubkey: owner, created_at: 0, tags: [], content: `${file.peer}:${file.root}` })
|
|
290
355
|
async function download (file, options = {}) {
|
|
@@ -359,10 +424,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
359
424
|
async setAvailable (value) {
|
|
360
425
|
available = value === true
|
|
361
426
|
stopOnlineRetry(); offlineSends.clear()
|
|
427
|
+
if (available) resetSendRetry()
|
|
428
|
+
else clearSendRetry()
|
|
362
429
|
lifecycle++
|
|
363
430
|
if (!available) { await messenger?.pause('signer'); return }
|
|
364
431
|
await ready()
|
|
365
|
-
for (const entry of entries.values()) if (entry
|
|
432
|
+
for (const entry of entries.values()) if (pendingRetryable(entry)) entry.failed = false
|
|
366
433
|
await messenger?.resume('storage')
|
|
367
434
|
return schedule()
|
|
368
435
|
},
|
|
@@ -382,7 +449,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
382
449
|
offlineSends.delete(id); watchOfflineSends()
|
|
383
450
|
await ready()
|
|
384
451
|
const entry = entries.get(id)
|
|
385
|
-
if (entry) { entry.failed = false; if (deniedPeers.delete(entry.peer)) await schedule(); return pump() }
|
|
452
|
+
if (entry) { entry.failed = false; resetSendRetry(); if (deniedPeers.delete(entry.peer)) await schedule(); return pump() }
|
|
386
453
|
},
|
|
387
454
|
async cancel (id) {
|
|
388
455
|
cancelled.add(id)
|
|
@@ -396,6 +463,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
396
463
|
async close () {
|
|
397
464
|
closed = true; available = false; lifecycle++
|
|
398
465
|
stopOnlineRetry(); offlineSends.clear(); activeDownloads.clear(); pendingPriorityRanges.clear()
|
|
466
|
+
clearSendRetry()
|
|
399
467
|
for (const controller of sendControllers.values()) controller.abort()
|
|
400
468
|
await Promise.allSettled([configuring, initialized])
|
|
401
469
|
await messenger?.pause('closed')
|