libp2r2p 0.10.18 → 0.10.20
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 +193 -35
- package/content-key/services/iykc-proof.js +1 -1
- package/idb-queue/index.js +66 -2
- package/nip46/services/client.js +5 -1
- package/nip46/services/transport.js +4 -2
- package/package.json +1 -1
- package/private-channel/helpers/rumor.js +26 -0
- package/private-channel/index.js +28 -14
- package/private-channel/services/received-chunks.js +3 -2
- package/private-message/index.js +333 -281
- package/private-messenger/index.js +328 -91
- package/private-messenger/services/channel-state.js +1 -1
- package/relay/services/events.js +1 -1
- package/relay/services/query.js +5 -2
- package/relay/services/relay-pool.js +161 -127
|
@@ -13,7 +13,7 @@ channels, keyPath "pubkey"
|
|
|
13
13
|
pubkey watched channel public key
|
|
14
14
|
value evolving channel-recovery state, including last-seen/watched times,
|
|
15
15
|
mode, relays, seeders, requested offline-recovery retention, offline
|
|
16
|
-
ranges, active offline-range start,
|
|
16
|
+
ranges, active offline-range start, recoveredThrough (last successful scan end),
|
|
17
17
|
per-seeder activity, and sent/received content-key usage
|
|
18
18
|
|
|
19
19
|
Changed channel records are upserted individually and explicit removals are
|
package/relay/services/events.js
CHANGED
|
@@ -58,7 +58,7 @@ async function fetchLatestEventsByRelay (relayToAuthors, { kinds, dTagsByPubkey,
|
|
|
58
58
|
const filter = { kinds, authors: dAuthors }
|
|
59
59
|
if (d) filter['#d'] = [d]
|
|
60
60
|
requests.push(getEvents(filter, [relay])
|
|
61
|
-
.then(response => ({ requested: new Set(dAuthors), events: response.result || [] })))
|
|
61
|
+
.then(response => ({ requested: new Set(dAuthors), events: (response.result || []).map(({ event }) => event) })))
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
package/relay/services/query.js
CHANGED
|
@@ -191,7 +191,7 @@ export function subscribeRelayListUpdates (pubkeys, {
|
|
|
191
191
|
|
|
192
192
|
async function consumeRelayListUpdates () {
|
|
193
193
|
try {
|
|
194
|
-
for await (const
|
|
194
|
+
for await (const item of _eventsFeedGenerator({
|
|
195
195
|
kinds: [10002],
|
|
196
196
|
authors
|
|
197
197
|
}, relays, {
|
|
@@ -199,6 +199,9 @@ export function subscribeRelayListUpdates (pubkeys, {
|
|
|
199
199
|
timeout: 5000,
|
|
200
200
|
timeoutAfterFirstEose: null
|
|
201
201
|
})) {
|
|
202
|
+
if (item.type === 'error') { console.error('relay-list watch failed:', item.error); continue }
|
|
203
|
+
if (item.type !== 'event') continue
|
|
204
|
+
const { event } = item
|
|
202
205
|
if (closed || !authors.includes(event.pubkey)) continue
|
|
203
206
|
const update = cacheRelayListEvent(event, { cacheMs, relayUrlPolicy })
|
|
204
207
|
if (!update || !relayTypeChanged(update.changes, relayType)) continue
|
|
@@ -238,7 +241,7 @@ async function loadMissingRelays (missingPubkeys, {
|
|
|
238
241
|
})
|
|
239
242
|
|
|
240
243
|
const latestByPubkey = {}
|
|
241
|
-
for (const event of events || []) {
|
|
244
|
+
for (const { event } of events || []) {
|
|
242
245
|
if (!missingPubkeys.includes(event.pubkey)) continue
|
|
243
246
|
if (isNewerRelayListEvent(event, latestByPubkey[event.pubkey])) latestByPubkey[event.pubkey] = event
|
|
244
247
|
}
|
|
@@ -80,6 +80,17 @@ function getEventsTimeoutError () {
|
|
|
80
80
|
return new Error('GET_EVENTS_TIMEOUT')
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
function asError (error) {
|
|
84
|
+
return error instanceof Error ? error : new Error(String(error))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertReadOptions (filter, timeouts) {
|
|
88
|
+
if (!filter || typeof filter !== 'object' || Array.isArray(filter)) throw new ValidationError('INVALID_FILTER')
|
|
89
|
+
for (const value of Object.values(timeouts)) {
|
|
90
|
+
if (value !== null && (!Number.isFinite(value) || value < 0)) throw new ValidationError('INVALID_RELAY_TIMEOUT')
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
83
94
|
function normalizedRelayUrls (relays) {
|
|
84
95
|
const urls = []
|
|
85
96
|
const seen = new Set()
|
|
@@ -88,7 +99,7 @@ function normalizedRelayUrls (relays) {
|
|
|
88
99
|
const normalizedUrl = normalizeRelayUrl(relay)
|
|
89
100
|
if (seen.has(normalizedUrl)) continue
|
|
90
101
|
seen.add(normalizedUrl)
|
|
91
|
-
// Keep the caller's first spelling in reports and
|
|
102
|
+
// Keep the caller's first spelling in reports and envelopes while using the
|
|
92
103
|
// canonical spelling for pooled connection ownership.
|
|
93
104
|
urls.push(relay)
|
|
94
105
|
}
|
|
@@ -342,127 +353,115 @@ export class RelayPool {
|
|
|
342
353
|
// the operation deadline. Disabling cross-relay deduplication still suppresses
|
|
343
354
|
// repeated ids from the same relay; callbacks remain immediate in both modes.
|
|
344
355
|
async getEvents (filter, relays, { timeout = 5000, timeoutAfterFirstEose = 500, callback, signal, deduplicateAcrossRelays = true } = {}) {
|
|
356
|
+
assertReadOptions(filter, { timeout, timeoutAfterFirstEose })
|
|
345
357
|
if (typeof deduplicateAcrossRelays !== 'boolean') throw new ValidationError('INVALID_DEDUPLICATE_ACROSS_RELAYS')
|
|
346
|
-
const urls = normalizedRelayUrls(relays)
|
|
347
|
-
if (!urls.length) return { result: [], errors: [], success: false }
|
|
348
358
|
if (signal?.aborted) throw new Error('Aborted')
|
|
349
|
-
|
|
359
|
+
const urls = normalizedRelayUrls(relays)
|
|
350
360
|
const subscriptions = new Map()
|
|
351
|
-
const
|
|
352
|
-
const normalCloseUrls = new Set()
|
|
361
|
+
const outcomes = new Map()
|
|
353
362
|
const errors = []
|
|
354
363
|
const events = []
|
|
355
364
|
const eventIds = deduplicateAcrossRelays ? new Set() : null
|
|
356
|
-
let completed = 0
|
|
357
365
|
let isResolved = false
|
|
358
366
|
let eoseTimer = null
|
|
359
367
|
let timeoutTimer = null
|
|
360
368
|
|
|
361
369
|
return await new Promise((resolve, reject) => {
|
|
362
|
-
const closeSubscriptions = () => {
|
|
363
|
-
for (const sub of subscriptions.values()) sub.close()
|
|
364
|
-
subscriptions.clear()
|
|
365
|
-
}
|
|
366
|
-
|
|
367
370
|
const cleanup = () => {
|
|
368
371
|
clearTimeout(timeoutTimer)
|
|
369
372
|
clearTimeout(eoseTimer)
|
|
370
373
|
signal?.removeEventListener('abort', onAbort)
|
|
371
|
-
|
|
374
|
+
for (const sub of subscriptions.values()) sub.close()
|
|
375
|
+
subscriptions.clear()
|
|
372
376
|
}
|
|
373
|
-
|
|
374
|
-
const finish = () => {
|
|
377
|
+
const fail = error => {
|
|
375
378
|
if (isResolved) return
|
|
376
379
|
isResolved = true
|
|
377
380
|
cleanup()
|
|
378
|
-
|
|
379
|
-
result: events,
|
|
380
|
-
errors,
|
|
381
|
-
success: events.length > 0 || completed > 0
|
|
382
|
-
})
|
|
381
|
+
reject(error)
|
|
383
382
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (pending.size === 0) finish()
|
|
383
|
+
const notify = item => {
|
|
384
|
+
try { callback?.(item) } catch (error) { fail(error) }
|
|
387
385
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (callback) callback({ type: 'error', error: reason, relay: url })
|
|
395
|
-
} else {
|
|
396
|
-
completed++
|
|
386
|
+
const settleRelay = (relay, status, error) => {
|
|
387
|
+
if (isResolved || outcomes.has(relay)) return
|
|
388
|
+
outcomes.set(relay, { relay, status, ...(error ? { error } : {}) })
|
|
389
|
+
if (error) {
|
|
390
|
+
errors.push({ relay, reason: error })
|
|
391
|
+
notify({ type: 'error', relay, error })
|
|
397
392
|
}
|
|
398
|
-
finishIfComplete()
|
|
399
393
|
}
|
|
400
|
-
|
|
401
|
-
|
|
394
|
+
const finish = (pendingStatus = 'cutoff') => {
|
|
395
|
+
if (isResolved) return
|
|
396
|
+
for (const url of urls) {
|
|
397
|
+
if (!outcomes.has(url)) settleRelay(url, pendingStatus, pendingStatus === 'timeout' ? getEventsTimeoutError() : undefined)
|
|
398
|
+
}
|
|
399
|
+
if (isResolved) return
|
|
400
|
+
const relays = urls.map(url => outcomes.get(url))
|
|
401
|
+
notify({ type: 'eose', relays })
|
|
402
402
|
if (isResolved) return
|
|
403
403
|
isResolved = true
|
|
404
404
|
cleanup()
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
errors.push({ reason: getEventsTimeoutError(), relay: url })
|
|
412
|
-
}
|
|
413
|
-
finish()
|
|
405
|
+
resolve({
|
|
406
|
+
result: events,
|
|
407
|
+
errors,
|
|
408
|
+
success: events.length > 0 || relays.some(({ status }) => ['eose', 'satisfied', 'closed'].includes(status)),
|
|
409
|
+
relays
|
|
410
|
+
})
|
|
414
411
|
}
|
|
415
|
-
|
|
412
|
+
const finishIfComplete = () => { if (outcomes.size === urls.length) finish() }
|
|
413
|
+
const onAbort = () => fail(new Error('Aborted'))
|
|
416
414
|
signal?.addEventListener('abort', onAbort, { once: true })
|
|
417
|
-
if (timeout !== null) timeoutTimer = maybeUnref(setTimeout(
|
|
415
|
+
if (timeout !== null) timeoutTimer = maybeUnref(setTimeout(() => finish('timeout'), timeout))
|
|
416
|
+
if (!urls.length) { finish(); return }
|
|
418
417
|
|
|
419
418
|
for (const url of urls) {
|
|
420
419
|
const seenIds = eventIds ?? new Set()
|
|
421
420
|
this.#getRelay(url).then(relay => {
|
|
422
|
-
if (isResolved ||
|
|
421
|
+
if (isResolved || outcomes.has(url)) return
|
|
423
422
|
let hasEvents = false
|
|
423
|
+
// Subscription callbacks may run before subscribe() returns.
|
|
424
424
|
// eslint-disable-next-line prefer-const
|
|
425
425
|
let sub
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
sub.close()
|
|
426
|
+
const complete = status => {
|
|
427
|
+
if (isResolved || outcomes.has(url)) return
|
|
428
|
+
settleRelay(url, status)
|
|
429
|
+
sub?.close()
|
|
430
|
+
subscriptions.delete(url)
|
|
432
431
|
if (hasEvents && timeoutAfterFirstEose !== null && !eoseTimer && !isResolved) {
|
|
433
|
-
eoseTimer = maybeUnref(setTimeout(finish, timeoutAfterFirstEose))
|
|
432
|
+
eoseTimer = maybeUnref(setTimeout(() => finish('cutoff'), timeoutAfterFirstEose))
|
|
434
433
|
}
|
|
434
|
+
finishIfComplete()
|
|
435
435
|
}
|
|
436
|
-
|
|
437
|
-
const checkEarlyClose = makeEarlyCloseChecker(filter, handleEose)
|
|
436
|
+
const checkEarlyClose = makeEarlyCloseChecker(filter, () => complete('satisfied'))
|
|
438
437
|
sub = relay.subscribe([filter], {
|
|
439
|
-
onevent:
|
|
440
|
-
if (isResolved ||
|
|
438
|
+
onevent: event => {
|
|
439
|
+
if (isResolved || outcomes.has(url)) return
|
|
441
440
|
hasEvents = true
|
|
442
441
|
if (!event?.id || !seenIds.has(event.id)) {
|
|
443
442
|
if (event?.id) seenIds.add(event.id)
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
if (callback) callback({ type: 'event', event, relay: url })
|
|
443
|
+
events.push({ event, relay: url })
|
|
444
|
+
notify({ type: 'event', event, relay: url })
|
|
447
445
|
}
|
|
448
446
|
checkEarlyClose(event)
|
|
449
447
|
},
|
|
450
448
|
oninvalidevent: () => {
|
|
451
|
-
if (!isResolved &&
|
|
449
|
+
if (!isResolved && !outcomes.has(url)) checkEarlyClose()
|
|
452
450
|
},
|
|
453
451
|
onclose: error => {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
452
|
+
if (isResolved || outcomes.has(url)) return
|
|
453
|
+
const reason = error === undefined ? undefined : asError(error)
|
|
454
|
+
settleRelay(url, reason ? 'error' : 'closed', reason)
|
|
455
|
+
subscriptions.delete(url)
|
|
456
|
+
finishIfComplete()
|
|
458
457
|
},
|
|
459
|
-
oneose:
|
|
458
|
+
oneose: () => complete('eose')
|
|
460
459
|
})
|
|
461
|
-
if (isResolved ||
|
|
460
|
+
if (isResolved || outcomes.has(url)) sub.close()
|
|
462
461
|
else subscriptions.set(url, sub)
|
|
463
462
|
}).catch(error => {
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
settleRelay(url, 'error', asError(error))
|
|
464
|
+
finishIfComplete()
|
|
466
465
|
})
|
|
467
466
|
}
|
|
468
467
|
})
|
|
@@ -472,39 +471,40 @@ export class RelayPool {
|
|
|
472
471
|
const queue = []
|
|
473
472
|
let p = Promise.withResolvers()
|
|
474
473
|
let isDone = false
|
|
475
|
-
|
|
476
|
-
const
|
|
474
|
+
let failure
|
|
475
|
+
const controller = new AbortController()
|
|
477
476
|
const callback = item => {
|
|
478
477
|
queue.push(item)
|
|
479
|
-
if (userCallback) userCallback(item)
|
|
480
478
|
p.resolve()
|
|
481
479
|
p = Promise.withResolvers()
|
|
480
|
+
options.callback?.(item)
|
|
482
481
|
}
|
|
483
|
-
|
|
484
|
-
//
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
|
|
482
|
+
const signal = AbortSignal.any([controller.signal, ...[options.signal, options._stopSignal].filter(Boolean)])
|
|
483
|
+
// Attach rejection handling immediately, but propagate general failures to
|
|
484
|
+
// the consumer after accepted deliveries instead of swallowing them.
|
|
485
|
+
const methodPromise = this.getEvents(filter, relays, { ...options, signal, callback })
|
|
486
|
+
.catch(error => { if (!signal.aborted) failure = error })
|
|
487
|
+
.finally(() => { isDone = true; p.resolve() })
|
|
488
|
+
try {
|
|
489
|
+
// eslint-disable-next-line no-unmodified-loop-condition
|
|
490
|
+
while (!isDone || queue.length > 0) {
|
|
491
|
+
if (options.signal?.aborted) return
|
|
492
|
+
if (queue.length > 0) yield queue.shift()
|
|
493
|
+
else await p.promise
|
|
494
|
+
}
|
|
495
|
+
const report = await methodPromise
|
|
496
|
+
if (failure) throw failure
|
|
497
|
+
return report
|
|
498
|
+
} finally {
|
|
499
|
+
controller.abort()
|
|
500
|
+
queue.length = 0
|
|
500
501
|
}
|
|
501
|
-
|
|
502
|
-
return await methodPromise
|
|
503
502
|
}
|
|
504
503
|
|
|
505
504
|
// Returns a strictly-live stream. `ready` reports the first initial EOSE window,
|
|
506
505
|
// while `readyRelays` follows relays that are currently past their own EOSE.
|
|
507
506
|
getLiveEventsGenerator (filter, relays, options = {}) {
|
|
507
|
+
assertReadOptions(filter, { timeout: options.timeout === undefined ? 5000 : options.timeout, timeoutAfterFirstEose: options.timeoutAfterFirstEose === undefined ? 500 : options.timeoutAfterFirstEose })
|
|
508
508
|
const ready = Promise.withResolvers()
|
|
509
509
|
const readyRelays = new Set()
|
|
510
510
|
const stream = drainableStream(options => this.#getLiveEventsGenerator(filter, relays, options, { ready, readyRelays }), options)
|
|
@@ -528,6 +528,7 @@ export class RelayPool {
|
|
|
528
528
|
async * #getLiveEventsGenerator (filter, relays, {
|
|
529
529
|
signal,
|
|
530
530
|
_stopSignal,
|
|
531
|
+
timeout = 5000,
|
|
531
532
|
timeoutAfterFirstEose = 500,
|
|
532
533
|
timeoutForReconnectGap = 5000,
|
|
533
534
|
timeoutAfterFirstReconnectGapEose = 500,
|
|
@@ -542,7 +543,8 @@ export class RelayPool {
|
|
|
542
543
|
const liveSubs = new Map() // url → live sub
|
|
543
544
|
const retryTimers = new Map()
|
|
544
545
|
const initialPending = new Set(urls)
|
|
545
|
-
const
|
|
546
|
+
const initialOutcomes = new Map()
|
|
547
|
+
let readyTimeout = null
|
|
546
548
|
let readyTimer = null
|
|
547
549
|
let isReady = false
|
|
548
550
|
|
|
@@ -564,14 +566,28 @@ export class RelayPool {
|
|
|
564
566
|
const seenIds = new Set()
|
|
565
567
|
|
|
566
568
|
let untilTimer = null
|
|
567
|
-
const
|
|
569
|
+
const enqueue = item => {
|
|
570
|
+
queue.push(item)
|
|
571
|
+
p.resolve()
|
|
572
|
+
p = Promise.withResolvers()
|
|
573
|
+
}
|
|
574
|
+
const finishReady = (pendingStatus = 'cutoff', emit = true) => {
|
|
568
575
|
if (isReady) return
|
|
569
576
|
isReady = true
|
|
570
577
|
clearTimeout(readyTimer)
|
|
578
|
+
clearTimeout(readyTimeout)
|
|
579
|
+
for (const relay of initialPending) {
|
|
580
|
+
const error = pendingStatus === 'timeout' ? getEventsTimeoutError() : undefined
|
|
581
|
+
initialOutcomes.set(relay, { relay, status: pendingStatus, ...(error ? { error } : {}) })
|
|
582
|
+
if (error && emit) enqueue({ type: 'error', relay, error })
|
|
583
|
+
}
|
|
584
|
+
initialPending.clear()
|
|
585
|
+
const relays = urls.map(url => initialOutcomes.get(url))
|
|
571
586
|
ready.resolve(Object.freeze({
|
|
572
587
|
relays: Object.freeze([...readyRelays]),
|
|
573
|
-
errors: Object.freeze(
|
|
588
|
+
errors: Object.freeze(relays.filter(item => item.error).map(({ relay, error }) => ({ relay, reason: error })))
|
|
574
589
|
}))
|
|
590
|
+
if (emit) enqueue({ type: 'eose', relays })
|
|
575
591
|
}
|
|
576
592
|
|
|
577
593
|
const teardown = (drain = false) => {
|
|
@@ -580,7 +596,7 @@ export class RelayPool {
|
|
|
580
596
|
isDone = true
|
|
581
597
|
if (!drain) queue.length = 0
|
|
582
598
|
clearTimeout(untilTimer)
|
|
583
|
-
finishReady()
|
|
599
|
+
finishReady('closed', false)
|
|
584
600
|
gapAc.abort()
|
|
585
601
|
for (const timer of retryTimers.values()) clearTimeout(timer)
|
|
586
602
|
retryTimers.clear()
|
|
@@ -597,14 +613,11 @@ export class RelayPool {
|
|
|
597
613
|
seenIds.add(event.id)
|
|
598
614
|
}
|
|
599
615
|
if (event.created_at > (lastSeenAt ?? 0)) lastSeenAt = event.created_at
|
|
600
|
-
event
|
|
601
|
-
queue.push(event)
|
|
602
|
-
p.resolve()
|
|
603
|
-
p = Promise.withResolvers()
|
|
616
|
+
enqueue({ type: 'event', event, relay: url })
|
|
604
617
|
}
|
|
605
618
|
|
|
606
619
|
if (signal?.aborted || _stopSignal?.aborted) {
|
|
607
|
-
finishReady()
|
|
620
|
+
finishReady('closed', false)
|
|
608
621
|
return
|
|
609
622
|
}
|
|
610
623
|
const abort = () => teardown()
|
|
@@ -612,24 +625,35 @@ export class RelayPool {
|
|
|
612
625
|
signal?.addEventListener('abort', abort, { once: true })
|
|
613
626
|
_stopSignal?.addEventListener('abort', stop, { once: true })
|
|
614
627
|
|
|
628
|
+
if (timeout !== null) readyTimeout = maybeUnref(setTimeout(() => finishReady('timeout'), timeout))
|
|
629
|
+
|
|
615
630
|
const maybeFinishInitialReady = () => {
|
|
616
631
|
if (initialPending.size === 0) finishReady()
|
|
617
632
|
}
|
|
618
633
|
|
|
619
634
|
const markInitialEose = (url) => {
|
|
620
635
|
readyRelays.add(url)
|
|
621
|
-
if (isReady) return
|
|
622
|
-
|
|
636
|
+
if (isReady || !initialPending.delete(url)) return
|
|
637
|
+
initialOutcomes.set(url, { relay: url, status: 'eose' })
|
|
623
638
|
if (timeoutAfterFirstEose !== null && !readyTimer) {
|
|
624
|
-
readyTimer = maybeUnref(setTimeout(finishReady, timeoutAfterFirstEose))
|
|
639
|
+
readyTimer = maybeUnref(setTimeout(() => finishReady('cutoff'), timeoutAfterFirstEose))
|
|
625
640
|
}
|
|
626
641
|
maybeFinishInitialReady()
|
|
627
642
|
}
|
|
628
643
|
|
|
629
|
-
const
|
|
630
|
-
if (
|
|
631
|
-
|
|
632
|
-
|
|
644
|
+
const reportFailure = (url, error) => {
|
|
645
|
+
if (isDone) return
|
|
646
|
+
enqueue({ type: 'error', relay: url, error })
|
|
647
|
+
if (!isReady && initialPending.delete(url)) {
|
|
648
|
+
initialOutcomes.set(url, { relay: url, status: 'error', error })
|
|
649
|
+
maybeFinishInitialReady()
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const reportClosed = url => {
|
|
653
|
+
if (!isReady && initialPending.delete(url)) {
|
|
654
|
+
initialOutcomes.set(url, { relay: url, status: 'closed' })
|
|
655
|
+
maybeFinishInitialReady()
|
|
656
|
+
}
|
|
633
657
|
}
|
|
634
658
|
|
|
635
659
|
const scheduleReconnect = (url, reconnectDelay) => {
|
|
@@ -645,7 +669,7 @@ export class RelayPool {
|
|
|
645
669
|
// Schedule teardown when the wall clock reaches filter.until
|
|
646
670
|
if (filterUntil !== null) {
|
|
647
671
|
const msUntil = filterUntil * 1000 - Date.now()
|
|
648
|
-
untilTimer = maybeUnref(setTimeout(() => teardown(true), Math.max(0, msUntil)))
|
|
672
|
+
untilTimer = maybeUnref(setTimeout(() => { finishReady('closed'); teardown(true) }, Math.max(0, msUntil)))
|
|
649
673
|
}
|
|
650
674
|
|
|
651
675
|
// Runs a reconnect gap fill for a single relay and returns a promise that resolves
|
|
@@ -662,9 +686,10 @@ export class RelayPool {
|
|
|
662
686
|
return (async () => {
|
|
663
687
|
for await (const item of gapGen) {
|
|
664
688
|
if (item?.type === 'event') pushEvent(item.event, url, true)
|
|
689
|
+
else if (item?.type === 'error' && !isDone) enqueue(item)
|
|
665
690
|
}
|
|
666
691
|
})().catch(err => {
|
|
667
|
-
|
|
692
|
+
reportFailure(url, asError(err))
|
|
668
693
|
})
|
|
669
694
|
}
|
|
670
695
|
|
|
@@ -697,11 +722,9 @@ export class RelayPool {
|
|
|
697
722
|
if (liveSubs.get(url) === liveSub) liveSubs.delete(url)
|
|
698
723
|
else if (liveSubs.has(url)) return
|
|
699
724
|
readyRelays.delete(url)
|
|
700
|
-
if (!
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
: new Error(error ? String(error) : 'LIVE_SUBSCRIPTION_CLOSED')
|
|
704
|
-
markInitialError(url, reason)
|
|
725
|
+
if (!isDone) {
|
|
726
|
+
if (error !== undefined) reportFailure(url, asError(error))
|
|
727
|
+
else if (!liveEose) reportClosed(url)
|
|
705
728
|
}
|
|
706
729
|
if (isDone) return
|
|
707
730
|
scheduleReconnect(url, reconnectDelay)
|
|
@@ -728,15 +751,19 @@ export class RelayPool {
|
|
|
728
751
|
}).catch(err => {
|
|
729
752
|
readyRelays.delete(url)
|
|
730
753
|
const reason = err instanceof Error ? err : new Error(String(err))
|
|
731
|
-
|
|
754
|
+
reportFailure(url, reason)
|
|
732
755
|
if (isDone) return
|
|
733
|
-
console.error(`Live subscription error at ${url}:`, reason)
|
|
734
756
|
scheduleReconnect(url, reconnectDelay)
|
|
735
757
|
})
|
|
736
758
|
}
|
|
737
759
|
|
|
738
760
|
if (!urls.length) {
|
|
739
761
|
finishReady()
|
|
762
|
+
try { yield queue.shift() } finally {
|
|
763
|
+
teardown()
|
|
764
|
+
signal?.removeEventListener('abort', abort)
|
|
765
|
+
_stopSignal?.removeEventListener('abort', stop)
|
|
766
|
+
}
|
|
740
767
|
return
|
|
741
768
|
}
|
|
742
769
|
|
|
@@ -773,6 +800,7 @@ export class RelayPool {
|
|
|
773
800
|
//
|
|
774
801
|
// All underlying generators are injectable for testing.
|
|
775
802
|
getEventsFeedGenerator (filter, relays, options = {}) {
|
|
803
|
+
assertReadOptions(filter, { timeout: options.timeout === undefined ? 5000 : options.timeout, timeoutAfterFirstEose: options.timeoutAfterFirstEose === undefined ? 500 : options.timeoutAfterFirstEose })
|
|
776
804
|
return drainableStream(options => this.#getEventsFeedGenerator(filter, relays, options), options)
|
|
777
805
|
}
|
|
778
806
|
|
|
@@ -790,14 +818,14 @@ export class RelayPool {
|
|
|
790
818
|
const gen = _eventsGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal, _stopSignal })
|
|
791
819
|
for await (const item of gen) {
|
|
792
820
|
if (signal.aborted) return
|
|
793
|
-
|
|
821
|
+
yield item
|
|
794
822
|
}
|
|
795
823
|
return
|
|
796
824
|
}
|
|
797
825
|
|
|
798
826
|
// limit:0 means "no stored events, live only" — skip the initial fetch.
|
|
799
827
|
if (filter.limit === 0) {
|
|
800
|
-
for await (const event of _liveGenerator(filter, relays, { signal, _stopSignal })) {
|
|
828
|
+
for await (const event of _liveGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal, _stopSignal })) {
|
|
801
829
|
if (signal.aborted) return
|
|
802
830
|
yield event
|
|
803
831
|
}
|
|
@@ -808,19 +836,22 @@ export class RelayPool {
|
|
|
808
836
|
// buffering incoming events before we query stored ones.
|
|
809
837
|
// Relays always send stored matching events before EOSE (unless limit:0),
|
|
810
838
|
// so the initial fetch + buffering is always needed.
|
|
811
|
-
const liveGen = _liveGenerator(filter, relays, { signal, _stopSignal })
|
|
839
|
+
const liveGen = _liveGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal, _stopSignal })
|
|
812
840
|
const liveBuffer = []
|
|
813
841
|
let liveDone = false
|
|
842
|
+
let liveFailure
|
|
814
843
|
let liveWake = Promise.withResolvers()
|
|
815
844
|
|
|
816
845
|
const bgLoop = (async () => {
|
|
817
846
|
try {
|
|
818
847
|
for await (const event of liveGen) {
|
|
819
848
|
if (signal.aborted) break
|
|
820
|
-
liveBuffer.push(event)
|
|
849
|
+
if (event.type !== 'eose') liveBuffer.push(event)
|
|
821
850
|
liveWake.resolve()
|
|
822
851
|
liveWake = Promise.withResolvers()
|
|
823
852
|
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
liveFailure = error
|
|
824
855
|
} finally {
|
|
825
856
|
liveDone = true
|
|
826
857
|
liveWake.resolve()
|
|
@@ -836,21 +867,23 @@ export class RelayPool {
|
|
|
836
867
|
if (signal.aborted) return
|
|
837
868
|
if (item?.type === 'event' && !seenIds.has(item.event.id)) {
|
|
838
869
|
seenIds.add(item.event.id)
|
|
839
|
-
yield item
|
|
840
|
-
}
|
|
870
|
+
yield item
|
|
871
|
+
} else if (item?.type !== 'event') yield item
|
|
841
872
|
}
|
|
842
873
|
|
|
843
874
|
// Flush buffered live events that arrived during the initial fetch, deduping
|
|
844
875
|
// against stored ones (overlap is possible around the fetch's until boundary)
|
|
845
876
|
while (liveBuffer.length > 0) {
|
|
846
877
|
if (signal.aborted) return
|
|
847
|
-
const
|
|
848
|
-
if (!seenIds.has(event.id)) {
|
|
849
|
-
seenIds.add(event.id)
|
|
850
|
-
yield
|
|
878
|
+
const item = liveBuffer.shift()
|
|
879
|
+
if (item.type !== 'event' || !seenIds.has(item.event.id)) {
|
|
880
|
+
if (item.type === 'event') seenIds.add(item.event.id)
|
|
881
|
+
yield item
|
|
851
882
|
}
|
|
852
883
|
}
|
|
853
884
|
|
|
885
|
+
seenIds.clear()
|
|
886
|
+
|
|
854
887
|
// Yield subsequent live events directly — no more overlap with stored events
|
|
855
888
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
856
889
|
while (!liveDone || liveBuffer.length > 0) {
|
|
@@ -860,6 +893,7 @@ export class RelayPool {
|
|
|
860
893
|
}
|
|
861
894
|
if (!liveDone) await liveWake.promise
|
|
862
895
|
}
|
|
896
|
+
if (liveFailure) throw liveFailure
|
|
863
897
|
} finally {
|
|
864
898
|
await liveGen.return()
|
|
865
899
|
await bgLoop
|