libp2r2p 0.11.16 → 0.11.18
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 +15 -1
- package/package.json +1 -1
- package/private-messenger/helpers/fallback-race.js +134 -0
- package/private-messenger/helpers/pause-recovery.js +80 -0
- package/private-messenger/helpers/send-routing.js +31 -5
- package/private-messenger/index.js +219 -47
- package/private-messenger/session/README.md +60 -9
- package/private-messenger/session/index.js +136 -32
- package/relay/helpers/publish.js +5 -1
- package/relay/services/relay-connection.js +31 -18
- package/relay/services/relay-pool.js +11 -4
|
@@ -5,11 +5,13 @@ import { getEventHash, isValidEvent, isSerializableEvent } from '../../event/ind
|
|
|
5
5
|
import { decodeIrfsChunk, IRFS_CHUNK_BYTES } from '../../irfs/index.js'
|
|
6
6
|
import { getIykcProofs } from '../../content-key/index.js'
|
|
7
7
|
import { onOnline } from '../../network/index.js'
|
|
8
|
+
import { ValidationError } from '../../error/index.js'
|
|
8
9
|
import { decodeFileMetadata } from '../../nip94/index.js'
|
|
9
10
|
import { createChatOutbox } from './helpers/work-storage.js'
|
|
10
11
|
import { messengerSigner } from './helpers/signer.js'
|
|
11
12
|
import { assertMessagePublished } from '../helpers/publication.js'
|
|
12
|
-
import { normalizeFallbackRelays } from '../helpers/send-routing.js'
|
|
13
|
+
import { normalizeFallbackRelays, normalizeFallbackDelay } from '../helpers/send-routing.js'
|
|
14
|
+
import { createPauseRecovery, isRecoverableStorageFailure } from '../helpers/pause-recovery.js'
|
|
13
15
|
|
|
14
16
|
export function wireEvent (value, owner) {
|
|
15
17
|
const event = { kind: value.kind, created_at: value.created_at, tags: structuredClone(value.tags), content: value.content, pubkey: value.pubkey || owner }
|
|
@@ -27,12 +29,21 @@ const SEND_RETRY_MAX_MS = 30000
|
|
|
27
29
|
const unavailableError = (error, signal) => Object.assign(new Error(error?.message || 'CHAT_UNAVAILABLE'), {
|
|
28
30
|
code: error?.code,
|
|
29
31
|
cause: error,
|
|
32
|
+
...(Array.isArray(error?.pauseReasons) ? { pauseReasons: [...error.pauseReasons] } : {}),
|
|
30
33
|
retryWhenAvailable: !signal?.aborted,
|
|
31
34
|
retryWhenOnline: false
|
|
32
35
|
})
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
function messengerStatusSnapshot (value) {
|
|
38
|
+
if (!value || typeof value.closed !== 'boolean' || typeof value.paused !== 'boolean' ||
|
|
39
|
+
!Array.isArray(value.pauseReasons) || value.pauseReasons.some(reason => typeof reason !== 'string' || !reason.trim()) ||
|
|
40
|
+
value.paused !== Boolean(value.pauseReasons.length)) throw new ValidationError('INVALID_MESSENGER_STATUS')
|
|
41
|
+
return { closed: value.closed, paused: value.paused, pauseReasons: [...value.pauseReasons] }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, 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' }) }) {
|
|
35
45
|
fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
46
|
+
fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
|
|
36
47
|
const reportedErrors = new WeakSet()
|
|
37
48
|
const reportError = error => {
|
|
38
49
|
if (error && (typeof error === 'object' || typeof error === 'function')) {
|
|
@@ -51,6 +62,33 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
51
62
|
const cancelled = new Set()
|
|
52
63
|
const sendControllers = new Map()
|
|
53
64
|
const offlineSends = new Set()
|
|
65
|
+
const pausedSends = new Set()
|
|
66
|
+
let messengerStatus
|
|
67
|
+
let messengerBinding
|
|
68
|
+
let storageBlocked = false
|
|
69
|
+
const inboxRecovery = createPauseRecovery({
|
|
70
|
+
attempt: async signal => { signal.throwIfAborted(); await drain(); if (storageBlocked) throw inboxError },
|
|
71
|
+
retryable: isRecoverableStorageFailure, onError: reportError,
|
|
72
|
+
setTimer: _setTimeout, clearTimer: _clearTimeout, random: _random
|
|
73
|
+
})
|
|
74
|
+
let inboxError
|
|
75
|
+
const stateChanged = state => {
|
|
76
|
+
if (closed) return
|
|
77
|
+
messengerStatus = messengerStatusSnapshot(state)
|
|
78
|
+
if (!available || state.closed) return
|
|
79
|
+
if (state.paused) {
|
|
80
|
+
for (const entry of entries.values()) {
|
|
81
|
+
if (entry.peer !== owner && entry.failed && entry.status === 'pending') pausedSends.add(entry.id)
|
|
82
|
+
}
|
|
83
|
+
if (![...entries.values()].some(entry => pendingRetryable(entry) && !pausedSends.has(entry.id))) clearSendRetry()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
clearSendRetry()
|
|
87
|
+
for (const id of pausedSends) { const entry = entries.get(id); if (entry) entry.failed = false }
|
|
88
|
+
pausedSends.clear()
|
|
89
|
+
// The callback can run inside construction/resume, before configure settles.
|
|
90
|
+
queueMicrotask(() => pump())
|
|
91
|
+
}
|
|
54
92
|
let stopOnline
|
|
55
93
|
let sendRetryTimer
|
|
56
94
|
let sendRetryDelay = SEND_RETRY_MIN_MS
|
|
@@ -70,12 +108,15 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
70
108
|
let storage
|
|
71
109
|
let initialized
|
|
72
110
|
let closed = false
|
|
111
|
+
let closeWork
|
|
73
112
|
let available = false
|
|
74
113
|
let lifecycle = 0
|
|
75
114
|
let configuring = Promise.resolve()
|
|
115
|
+
let configurationFailed = false
|
|
76
116
|
let sending
|
|
77
117
|
let repump
|
|
78
118
|
let draining
|
|
119
|
+
let redrain = false
|
|
79
120
|
const idleWaiters = new Set()
|
|
80
121
|
const settled = () => { if (!sending && !draining) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
81
122
|
const emit = () => onOutbox([...entries.values()].filter(entry => !entry.deletion))
|
|
@@ -84,6 +125,35 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
84
125
|
for (const entry of await storage.list()) entries.set(entry.id, entry)
|
|
85
126
|
emit()
|
|
86
127
|
})().catch(async error => { await storage?.close(); storage = null; initialized = null; throw error }))
|
|
128
|
+
async function createMessenger () {
|
|
129
|
+
const binding = {}
|
|
130
|
+
messengerBinding = binding
|
|
131
|
+
let notified = false
|
|
132
|
+
let candidate
|
|
133
|
+
try {
|
|
134
|
+
candidate = await Messenger({
|
|
135
|
+
fallbackRelays, fallbackDelayMs, seedStorage: recoveryStorage?.seeds, userSigner, channels: [], useContentKeys,
|
|
136
|
+
onMessageQueued: () => { if (messengerBinding === binding) return drain() },
|
|
137
|
+
onStateChanged: state => {
|
|
138
|
+
if (closed || messengerBinding !== binding) return
|
|
139
|
+
stateChanged(state)
|
|
140
|
+
notified = true
|
|
141
|
+
},
|
|
142
|
+
onError: reportError
|
|
143
|
+
})
|
|
144
|
+
if (closed) { messengerBinding = null; await candidate?.close?.(); return }
|
|
145
|
+
if (typeof candidate?.readStatus !== 'function' || !notified) throw new ValidationError('MESSENGER_STATE_CONTRACT_REQUIRED')
|
|
146
|
+
const status = messengerStatusSnapshot(candidate.readStatus())
|
|
147
|
+
messenger = candidate
|
|
148
|
+
stateChanged(status)
|
|
149
|
+
} catch (error) {
|
|
150
|
+
messengerBinding = null
|
|
151
|
+
messengerStatus = undefined
|
|
152
|
+
try { await candidate?.close?.() } catch (cleanupError) { reportError(cleanupError) }
|
|
153
|
+
throw error
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
87
157
|
async function configure () {
|
|
88
158
|
if (closed || !available) return
|
|
89
159
|
await ready()
|
|
@@ -102,7 +172,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
102
172
|
if (closed || !available || version !== lifecycle) return
|
|
103
173
|
values.push(channel)
|
|
104
174
|
}
|
|
105
|
-
if (!messenger)
|
|
175
|
+
if (!messenger) await createMessenger()
|
|
176
|
+
if (!messenger) return
|
|
106
177
|
if (closed || !available || version !== lifecycle) { await messenger.pause('signer'); return }
|
|
107
178
|
await messenger.update({ channels: values })
|
|
108
179
|
if (pendingPriorityRanges.size && typeof messenger.prioritizeRange === 'function') {
|
|
@@ -116,6 +187,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
116
187
|
}
|
|
117
188
|
}
|
|
118
189
|
await messenger.resume('signer')
|
|
190
|
+
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
119
191
|
if (!fileTransfers) {
|
|
120
192
|
fileTransfers = FileTransfer({
|
|
121
193
|
messenger, onError: reportError,
|
|
@@ -153,13 +225,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
153
225
|
function schedule () {
|
|
154
226
|
const work = configuring.catch(() => {}).then(configure)
|
|
155
227
|
configuring = work
|
|
156
|
-
work.
|
|
228
|
+
work.then(() => { if (configuring === work) configurationFailed = false }, error => { if (configuring === work) configurationFailed = true; reportError(error) })
|
|
157
229
|
return work
|
|
158
230
|
}
|
|
159
231
|
async function drain () {
|
|
160
|
-
if (draining
|
|
232
|
+
if (draining) { redrain = true; return }
|
|
233
|
+
if (closed || !available || !messenger) return
|
|
161
234
|
draining = true
|
|
162
235
|
const deferred = []
|
|
236
|
+
let persisted = false
|
|
237
|
+
let passFailed = false
|
|
163
238
|
try {
|
|
164
239
|
while (true) {
|
|
165
240
|
if (closed || !available) break
|
|
@@ -181,16 +256,33 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
181
256
|
if (!available || closed) { await nack(); break }
|
|
182
257
|
const saved = await messageStorage.save(event, { peerPubkey: peer, hearsay: message.provenance === 'hearsay' })
|
|
183
258
|
if (!saved?.result?.ok && !['blocked', 'expired'].includes(saved?.result?.code)) throw Object.assign(new Error('INBOX_STORAGE_FAILED'), { code: saved?.result?.code?.toUpperCase() })
|
|
184
|
-
await
|
|
259
|
+
if (!available || closed) { await nack(); break }
|
|
260
|
+
if (await ack() === false) throw new Error('INBOX_ACK_FAILED')
|
|
261
|
+
persisted = true
|
|
185
262
|
} catch (error) {
|
|
186
263
|
await nack()
|
|
187
264
|
reportError(error)
|
|
188
265
|
// Retain the failed record and suspend network ingestion until recovery.
|
|
189
|
-
|
|
266
|
+
passFailed = true
|
|
267
|
+
storageBlocked = true
|
|
268
|
+
inboxError = error
|
|
269
|
+
await messenger.pause('session-storage')
|
|
270
|
+
if (isRecoverableStorageFailure(error)) inboxRecovery.start()
|
|
190
271
|
break
|
|
191
272
|
}
|
|
192
273
|
}
|
|
193
|
-
|
|
274
|
+
if (storageBlocked && persisted && !passFailed) {
|
|
275
|
+
// A retry can save one record and fail the next. Only release after the
|
|
276
|
+
// whole currently admissible pass completes without another failure.
|
|
277
|
+
storageBlocked = false; inboxError = null
|
|
278
|
+
await messenger.resume('session-storage')
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
await Promise.all(deferred.map(nack => nack()))
|
|
282
|
+
draining = false; settled()
|
|
283
|
+
const again = redrain; redrain = false
|
|
284
|
+
if (again && !storageBlocked && !closed && available) queueMicrotask(() => drain())
|
|
285
|
+
}
|
|
194
286
|
}
|
|
195
287
|
async function publish (peer, event, signal) {
|
|
196
288
|
if (deniedPeers.has(peer)) throw new Error('PERMISSION_DENIED')
|
|
@@ -229,6 +321,9 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
229
321
|
await storage.put(entry, { existing: true })
|
|
230
322
|
}
|
|
231
323
|
}
|
|
324
|
+
if (entry.peer !== owner && messengerStatus?.paused) {
|
|
325
|
+
throw unavailableError(Object.assign(new Error('PRIVATE_MESSENGER_PAUSED'), { pauseReasons: [...messengerStatus.pauseReasons] }), controller.signal)
|
|
326
|
+
}
|
|
232
327
|
// Read/publish one chunk at a time. Progress is durable after each
|
|
233
328
|
// accepted chunk; replay after interruption keeps its original identity.
|
|
234
329
|
for (; entry.peer !== owner && entry.fileIndex < entry.files.length; entry.fileIndex++, entry.chunkIndex = 0) {
|
|
@@ -273,20 +368,20 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
273
368
|
if (cancelled.has(entry.id)) { await storage.remove(entry.id); entries.delete(entry.id) } else {
|
|
274
369
|
entry.status = error.retryWhenAvailable ? 'pending' : 'error'; entry.failed = true; entry.retryable = retryable(error)
|
|
275
370
|
await storage.put(entry, { existing: true }).catch(reportError)
|
|
371
|
+
if (entry.peer !== owner && error.retryWhenAvailable && messenger.readStatus().paused) pausedSends.add(entry.id)
|
|
276
372
|
reportError(error)
|
|
277
373
|
// The failed wire event may be a quote or file chunk. Report the
|
|
278
374
|
// owning outbox item separately, without mutating the native error.
|
|
279
375
|
if (error.retryWhenOnline) offlineSends.add(entry.id)
|
|
280
|
-
if (error.retryWhenAvailable) scheduleSendRetry()
|
|
376
|
+
if (error.retryWhenAvailable && !pausedSends.has(entry.id)) scheduleSendRetry()
|
|
281
377
|
if (!closed && !entry.deletion && !cancelled.has(entry.id) && !error.retryWhenAvailable) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
282
378
|
}
|
|
283
379
|
emit()
|
|
284
380
|
} finally { sendControllers.delete(entry.id) }
|
|
285
381
|
}
|
|
286
382
|
function stopOnlineRetry () { stopOnline?.(); stopOnline = undefined }
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// instead of waiting for the next account-state change.
|
|
383
|
+
// Transient availability failures without an active messenger pause retain
|
|
384
|
+
// bounded retries. Observed pauses wait exclusively for their release.
|
|
290
385
|
function clearSendRetry () {
|
|
291
386
|
if (sendRetryTimer) _clearTimeout(sendRetryTimer)
|
|
292
387
|
sendRetryTimer = undefined
|
|
@@ -305,7 +400,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
305
400
|
if (closed || !available) return
|
|
306
401
|
let retried = false
|
|
307
402
|
for (const entry of entries.values()) {
|
|
308
|
-
if (!pendingRetryable(entry)) continue
|
|
403
|
+
if (!pendingRetryable(entry) || pausedSends.has(entry.id)) continue
|
|
309
404
|
entry.failed = false
|
|
310
405
|
retried = true
|
|
311
406
|
}
|
|
@@ -341,9 +436,10 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
341
436
|
for (const entry of entries.values()) {
|
|
342
437
|
if (closed || !available) break
|
|
343
438
|
if ((entry.peer !== owner && !peers.has(entry.peer)) || entry.failed || cancelled.has(entry.id)) continue
|
|
439
|
+
if (entry.peer !== owner && entry.files.length && !fileTransfers) continue
|
|
344
440
|
await sendEntry(entry)
|
|
345
441
|
}
|
|
346
|
-
} while (repump
|
|
442
|
+
} while (repump)
|
|
347
443
|
} finally {
|
|
348
444
|
sending = false
|
|
349
445
|
watchOfflineSends()
|
|
@@ -416,21 +512,26 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
416
512
|
return true
|
|
417
513
|
},
|
|
418
514
|
async setPeers (values) {
|
|
419
|
-
|
|
515
|
+
const next = new Set([...values].filter(peer => peer !== owner))
|
|
516
|
+
if (next.size === peers.size && [...next].every(peer => peers.has(peer))) return configuring
|
|
517
|
+
peers.clear(); for (const peer of next) peers.add(peer)
|
|
420
518
|
for (const peer of deniedPeers) if (!peers.has(peer)) deniedPeers.delete(peer)
|
|
421
519
|
lifecycle++
|
|
422
520
|
return schedule()
|
|
423
521
|
},
|
|
424
522
|
async setAvailable (value) {
|
|
523
|
+
const changed = available !== (value === true)
|
|
425
524
|
available = value === true
|
|
525
|
+
if (!changed && !available) return
|
|
426
526
|
stopOnlineRetry(); offlineSends.clear()
|
|
427
527
|
if (available) resetSendRetry()
|
|
428
528
|
else clearSendRetry()
|
|
429
|
-
lifecycle++
|
|
430
|
-
if (!available) { await messenger?.pause('signer'); return }
|
|
529
|
+
if (changed) lifecycle++
|
|
530
|
+
if (!available) { inboxRecovery.stop(); await messenger?.pause('signer'); return }
|
|
431
531
|
await ready()
|
|
432
532
|
for (const entry of entries.values()) if (pendingRetryable(entry)) entry.failed = false
|
|
433
|
-
|
|
533
|
+
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
534
|
+
if (!changed && messenger && !configurationFailed) return pump()
|
|
434
535
|
return schedule()
|
|
435
536
|
},
|
|
436
537
|
async enqueue ({ peer, event, context = [], requiredFiles = [], deletion = false }) {
|
|
@@ -446,7 +547,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
446
547
|
return id
|
|
447
548
|
},
|
|
448
549
|
async retry (id) {
|
|
449
|
-
offlineSends.delete(id); watchOfflineSends()
|
|
550
|
+
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
551
|
+
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
450
552
|
await ready()
|
|
451
553
|
const entry = entries.get(id)
|
|
452
554
|
if (entry) { entry.failed = false; resetSendRetry(); if (deniedPeers.delete(entry.peer)) await schedule(); return pump() }
|
|
@@ -454,24 +556,26 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
454
556
|
async cancel (id) {
|
|
455
557
|
cancelled.add(id)
|
|
456
558
|
sendControllers.get(id)?.abort()
|
|
457
|
-
offlineSends.delete(id); watchOfflineSends()
|
|
559
|
+
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
458
560
|
await ready()
|
|
459
561
|
entries.delete(id)
|
|
460
562
|
await storage.remove(id)
|
|
461
563
|
emit()
|
|
462
564
|
},
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
565
|
+
close () {
|
|
566
|
+
return (closeWork ??= (async () => {
|
|
567
|
+
closed = true; available = false; lifecycle++
|
|
568
|
+
stopOnlineRetry(); offlineSends.clear(); activeDownloads.clear(); pendingPriorityRanges.clear()
|
|
569
|
+
clearSendRetry(); inboxRecovery.stop(); pausedSends.clear()
|
|
570
|
+
for (const controller of sendControllers.values()) controller.abort()
|
|
571
|
+
await Promise.allSettled([configuring, initialized])
|
|
572
|
+
await messenger?.pause('closed')
|
|
573
|
+
if (sending || draining) await new Promise(resolve => idleWaiters.add(resolve))
|
|
574
|
+
await messenger?.close()
|
|
575
|
+
await storage?.close()
|
|
576
|
+
await downloadWriteTail.catch(reportError)
|
|
577
|
+
await downloads?.close()
|
|
578
|
+
})())
|
|
475
579
|
}
|
|
476
580
|
}
|
|
477
581
|
}
|
package/relay/helpers/publish.js
CHANGED
|
@@ -76,7 +76,11 @@ export function createPublishSettlements (promises, timeout, { onSettled } = {})
|
|
|
76
76
|
})
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
const cancel = reason => {
|
|
80
|
+
if (isFinished) return
|
|
81
|
+
for (let index = 0; index < settlements.length; index++) settle(index, { status: 'rejected', outcome: 'failed', reason })
|
|
82
|
+
}
|
|
83
|
+
return { promise, timeout: timeoutPending, cancel }
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
// Turns ordered relay settlements into a stable, caller-facing report. Failed
|
|
@@ -186,23 +186,33 @@ export class RelayConnection {
|
|
|
186
186
|
}
|
|
187
187
|
|
|
188
188
|
#sendEventOperation (type, event, map, timeoutCode, signal) {
|
|
189
|
-
if (signal?.aborted) return Promise.reject(signal.reason
|
|
190
|
-
|
|
189
|
+
if (signal?.aborted) return Promise.reject(signal.reason)
|
|
190
|
+
let pending = map.get(event.id)
|
|
191
|
+
const created = !pending
|
|
192
|
+
if (created) {
|
|
193
|
+
const timer = maybeUnref(setTimeout(() => this.#settleEvent(map, event.id, relayTimeoutError(timeoutCode, this.#lastTransportError)), this.publishTimeout))
|
|
194
|
+
pending = { timer, waiters: new Set(), cancelSend: null }
|
|
195
|
+
map.set(event.id, pending)
|
|
196
|
+
}
|
|
191
197
|
const deferred = Promise.withResolvers()
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
198
|
+
const onAbort = () => {
|
|
199
|
+
pending.waiters.delete(waiter)
|
|
200
|
+
signal.removeEventListener('abort', onAbort)
|
|
201
|
+
deferred.reject(signal.reason)
|
|
202
|
+
if (!pending.waiters.size && map.get(event.id) === pending) {
|
|
203
|
+
map.delete(event.id)
|
|
204
|
+
pending.cancelSend?.()
|
|
205
|
+
clearTimeout(pending.timer)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const waiter = { ...deferred, stopAbort: () => signal?.removeEventListener('abort', onAbort) }
|
|
209
|
+
pending.waiters.add(waiter)
|
|
197
210
|
signal?.addEventListener('abort', onAbort, { once: true })
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}, error => {
|
|
204
|
-
this.#settleEvent(map, event.id, error)
|
|
205
|
-
})
|
|
211
|
+
if (created) {
|
|
212
|
+
pending.cancelSend = this.#dispatchWork(() => {
|
|
213
|
+
this.send(JSON.stringify([type, event]))
|
|
214
|
+
}, error => this.#settleEvent(map, event.id, error))
|
|
215
|
+
}
|
|
206
216
|
return deferred.promise
|
|
207
217
|
}
|
|
208
218
|
|
|
@@ -225,10 +235,13 @@ export class RelayConnection {
|
|
|
225
235
|
if (!pending) return
|
|
226
236
|
map.delete(id)
|
|
227
237
|
pending.cancelSend?.()
|
|
228
|
-
pending.stopAbort?.()
|
|
229
238
|
clearTimeout(pending.timer)
|
|
230
|
-
|
|
231
|
-
|
|
239
|
+
for (const waiter of pending.waiters) {
|
|
240
|
+
waiter.stopAbort()
|
|
241
|
+
if (reason) waiter.reject(errorFrom(reason, 'OPERATION_REJECTED'))
|
|
242
|
+
else waiter.resolve(value)
|
|
243
|
+
}
|
|
244
|
+
pending.waiters.clear()
|
|
232
245
|
}
|
|
233
246
|
|
|
234
247
|
#settleCount (id, payload, reason) {
|
|
@@ -1204,8 +1204,10 @@ export class RelayPool {
|
|
|
1204
1204
|
timeout = SEND_TIMEOUT_MS,
|
|
1205
1205
|
timeoutUntilFirstFulfillment = null,
|
|
1206
1206
|
getAuthEvent,
|
|
1207
|
-
onRelayResult
|
|
1207
|
+
onRelayResult,
|
|
1208
|
+
signal
|
|
1208
1209
|
} = {}) {
|
|
1210
|
+
if (signal != null && (typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function' || typeof signal.removeEventListener !== 'function')) throw new ValidationError('INVALID_RELAY_SIGNAL')
|
|
1209
1211
|
const urls = normalizedRelayUrls(relays)
|
|
1210
1212
|
if (!urls.length) {
|
|
1211
1213
|
const promise = Promise.resolve(publishSummary([], urls, {
|
|
@@ -1236,7 +1238,11 @@ export class RelayPool {
|
|
|
1236
1238
|
// Resolves after every relay settles (or reaches the operation timeout) as
|
|
1237
1239
|
// { success, total, fulfilled, succeededRelays, errors },
|
|
1238
1240
|
// where errors contains { relay, reason } entries for failed relays.
|
|
1241
|
+
const cancel = () => settlement.cancel(signal.reason)
|
|
1242
|
+
if (signal?.aborted) cancel()
|
|
1243
|
+
else signal?.addEventListener('abort', cancel, { once: true })
|
|
1239
1244
|
const promise = settlement.promise
|
|
1245
|
+
.finally(() => signal?.removeEventListener('abort', cancel))
|
|
1240
1246
|
.then(settlements => publishSummary(settlements, urls, {
|
|
1241
1247
|
includeSucceededRelays: true
|
|
1242
1248
|
}))
|
|
@@ -1245,10 +1251,11 @@ export class RelayPool {
|
|
|
1245
1251
|
const deferred = sendDeferreds[index]
|
|
1246
1252
|
;(async () => {
|
|
1247
1253
|
try {
|
|
1254
|
+
const operationSignal = sendControllers[index].signal
|
|
1255
|
+
if (operationSignal.aborted) throw operationSignal.reason
|
|
1248
1256
|
const relay = await this.#getRelay(url)
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
return await this.#publishEvent(relay, eventToSend, getAuthEvent, signal)
|
|
1257
|
+
if (operationSignal.aborted) throw operationSignal.reason
|
|
1258
|
+
return await this.#publishEvent(relay, eventToSend, getAuthEvent, operationSignal)
|
|
1252
1259
|
} catch (err) {
|
|
1253
1260
|
const reason = err instanceof Error ? err : new Error(String(err))
|
|
1254
1261
|
if (reason instanceof Nip42AuthenticationError) throw reason
|