libp2r2p 0.10.12 → 0.10.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 +29 -0
- package/package.json +1 -1
- package/relay/helpers/drainable-stream.js +17 -0
- package/relay/services/relay-pool.js +60 -26
package/README.md
CHANGED
|
@@ -12,6 +12,35 @@ For remote-work scheduling, see [`libp2r2p/network`](network/README.md):
|
|
|
12
12
|
`isOnline` probes connectivity and `onOnline` shares recovery monitoring,
|
|
13
13
|
including retries when the browser omits its native `online` event.
|
|
14
14
|
|
|
15
|
+
## Relay feed lifecycle
|
|
16
|
+
|
|
17
|
+
`RelayPool.getEventsFeedGenerator(filter, relays, options)` yields events directly,
|
|
18
|
+
including the existing `event.meta.relay` field. By default it combines an initial
|
|
19
|
+
historical query with a live subscription. `live: false` selects history only;
|
|
20
|
+
`filter.limit: 0` selects live only. `getLiveEventsGenerator` exposes the same
|
|
21
|
+
lifecycle controls along with its existing `ready` and `readyRelays` properties.
|
|
22
|
+
|
|
23
|
+
The returned iterator has a synchronous, idempotent `stopAndDrain()` method. It
|
|
24
|
+
closes subscription input and cancels reconnections and outstanding historical
|
|
25
|
+
queries, while retaining events already accepted by the library's receive
|
|
26
|
+
callbacks. Continue consuming the iterator to obtain those events and observe
|
|
27
|
+
completion. This includes initial history, buffered live events, and both history
|
|
28
|
+
and live buffers from reconnect recovery. Calling it before the first `next()`
|
|
29
|
+
prevents subscriptions from opening. No reception timestamp or new event metadata
|
|
30
|
+
is added. Events rejected by ordinary filtering/deduplication remain excluded.
|
|
31
|
+
|
|
32
|
+
Aborting `options.signal`, calling `return()` (including a `for await` break), or
|
|
33
|
+
calling `throw()` cancels input and pending delivery instead. An event already
|
|
34
|
+
delivered to the caller cannot be recalled. These operations also interrupt a
|
|
35
|
+
drain. `stopAndDrain()` does not consume the iterator or return a completion
|
|
36
|
+
promise: completion is the iterator's `{ done: true }` result.
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
const stream = relayPool.getEventsFeedGenerator({ authors: [pubkey] }, [relay], { signal })
|
|
40
|
+
// When this relay is removed, call stream.stopAndDrain() from the list handler.
|
|
41
|
+
for await (const event of stream) await store(event)
|
|
42
|
+
```
|
|
43
|
+
|
|
15
44
|
## Private Messenger
|
|
16
45
|
|
|
17
46
|
The main API is `createPrivateMessenger` from `libp2r2p/private-messenger`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Stopping closes network input but keeps accepted events available to next().
|
|
2
|
+
// Returning/throwing, or aborting the caller signal, cancels consumption as well.
|
|
3
|
+
export function drainableStream (create, options = {}) {
|
|
4
|
+
const cancel = new AbortController()
|
|
5
|
+
const stop = new AbortController()
|
|
6
|
+
const signal = options.signal ? AbortSignal.any([options.signal, cancel.signal]) : cancel.signal
|
|
7
|
+
const stopSignal = options._stopSignal ? AbortSignal.any([options._stopSignal, stop.signal]) : stop.signal
|
|
8
|
+
const stream = create({ ...options, signal, _stopSignal: stopSignal })
|
|
9
|
+
const returnStream = stream.return.bind(stream)
|
|
10
|
+
const throwStream = stream.throw.bind(stream)
|
|
11
|
+
Object.defineProperties(stream, {
|
|
12
|
+
stopAndDrain: { value: () => stop.abort() },
|
|
13
|
+
return: { value: value => { cancel.abort(); return returnStream(value) } },
|
|
14
|
+
throw: { value: error => { cancel.abort(); return throwStream(error) } }
|
|
15
|
+
})
|
|
16
|
+
return stream
|
|
17
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ValidationError } from '../../error/index.js'
|
|
2
2
|
import { decodeHll, encodeHll, estimateHllCount, mergeHll } from '../helpers/hll.js'
|
|
3
3
|
import { createPublishSettlements, firstFulfillment, publishSummary } from '../helpers/publish.js'
|
|
4
|
+
import { drainableStream } from '../helpers/drainable-stream.js'
|
|
4
5
|
import { maybeUnref } from '../helpers/timer.js'
|
|
5
6
|
import { categorizeRelayError } from '../helpers/error.js'
|
|
6
7
|
import { normalizeRelayUrl } from '../../url/index.js'
|
|
@@ -480,7 +481,11 @@ export class RelayPool {
|
|
|
480
481
|
p = Promise.withResolvers()
|
|
481
482
|
}
|
|
482
483
|
|
|
483
|
-
|
|
484
|
+
// A drain stops receive callbacks without aborting consumption of this queue.
|
|
485
|
+
const networkSignal = options._stopSignal
|
|
486
|
+
? AbortSignal.any([options._stopSignal, ...(options.signal ? [options.signal] : [])])
|
|
487
|
+
: options.signal
|
|
488
|
+
const methodPromise = this.getEvents(filter, relays, { ...options, signal: networkSignal, callback })
|
|
484
489
|
.catch(err => { if (err?.message !== 'Aborted') console.error('Error in getEvents:', err) })
|
|
485
490
|
.finally(() => {
|
|
486
491
|
isDone = true
|
|
@@ -489,6 +494,7 @@ export class RelayPool {
|
|
|
489
494
|
|
|
490
495
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
491
496
|
while (!isDone || queue.length > 0) {
|
|
497
|
+
if (options.signal?.aborted) break
|
|
492
498
|
if (queue.length > 0) yield queue.shift()
|
|
493
499
|
else await p.promise
|
|
494
500
|
}
|
|
@@ -501,7 +507,7 @@ export class RelayPool {
|
|
|
501
507
|
getLiveEventsGenerator (filter, relays, options = {}) {
|
|
502
508
|
const ready = Promise.withResolvers()
|
|
503
509
|
const readyRelays = new Set()
|
|
504
|
-
const stream = this.#getLiveEventsGenerator(filter, relays, options, { ready, readyRelays })
|
|
510
|
+
const stream = drainableStream(options => this.#getLiveEventsGenerator(filter, relays, options, { ready, readyRelays }), options)
|
|
505
511
|
|
|
506
512
|
Object.defineProperties(stream, {
|
|
507
513
|
ready: {
|
|
@@ -521,6 +527,7 @@ export class RelayPool {
|
|
|
521
527
|
// suppressing already-live events from another relay.
|
|
522
528
|
async * #getLiveEventsGenerator (filter, relays, {
|
|
523
529
|
signal,
|
|
530
|
+
_stopSignal,
|
|
524
531
|
timeoutAfterFirstEose = 500,
|
|
525
532
|
timeoutForReconnectGap = 5000,
|
|
526
533
|
timeoutAfterFirstReconnectGapEose = 500,
|
|
@@ -530,6 +537,8 @@ export class RelayPool {
|
|
|
530
537
|
const queue = []
|
|
531
538
|
let p = Promise.withResolvers()
|
|
532
539
|
let isDone = false
|
|
540
|
+
let draining = false
|
|
541
|
+
const gapTasks = new Set()
|
|
533
542
|
const liveSubs = new Map() // url → live sub
|
|
534
543
|
const retryTimers = new Map()
|
|
535
544
|
const initialPending = new Set(urls)
|
|
@@ -537,7 +546,7 @@ export class RelayPool {
|
|
|
537
546
|
let readyTimer = null
|
|
538
547
|
let isReady = false
|
|
539
548
|
|
|
540
|
-
//
|
|
549
|
+
// Stop recovery input on teardown, preserving its accepted history for a drain.
|
|
541
550
|
const gapAc = new AbortController()
|
|
542
551
|
|
|
543
552
|
// Strip time-range fields — we manage them internally
|
|
@@ -565,9 +574,11 @@ export class RelayPool {
|
|
|
565
574
|
}))
|
|
566
575
|
}
|
|
567
576
|
|
|
568
|
-
const teardown = () => {
|
|
569
|
-
if (isDone) return
|
|
577
|
+
const teardown = (drain = false) => {
|
|
578
|
+
if (isDone && (drain || !draining)) return
|
|
579
|
+
draining = drain
|
|
570
580
|
isDone = true
|
|
581
|
+
if (!drain) queue.length = 0
|
|
571
582
|
clearTimeout(untilTimer)
|
|
572
583
|
finishReady()
|
|
573
584
|
gapAc.abort()
|
|
@@ -578,8 +589,9 @@ export class RelayPool {
|
|
|
578
589
|
p.resolve()
|
|
579
590
|
}
|
|
580
591
|
|
|
581
|
-
const pushEvent = (event, url) => {
|
|
582
|
-
|
|
592
|
+
const pushEvent = (event, url, accepted = false) => {
|
|
593
|
+
// Recovery queues accepted these events before input was stopped.
|
|
594
|
+
if ((isDone && !(draining && accepted)) || (event.id && seenIds.has(event.id))) return
|
|
583
595
|
if (event.id) {
|
|
584
596
|
if (seenIds.size >= 500) seenIds.delete(seenIds.values().next().value) // evict oldest
|
|
585
597
|
seenIds.add(event.id)
|
|
@@ -591,11 +603,14 @@ export class RelayPool {
|
|
|
591
603
|
p = Promise.withResolvers()
|
|
592
604
|
}
|
|
593
605
|
|
|
594
|
-
if (signal?.aborted) {
|
|
606
|
+
if (signal?.aborted || _stopSignal?.aborted) {
|
|
595
607
|
finishReady()
|
|
596
608
|
return
|
|
597
609
|
}
|
|
598
|
-
|
|
610
|
+
const abort = () => teardown()
|
|
611
|
+
const stop = () => teardown(true)
|
|
612
|
+
signal?.addEventListener('abort', abort, { once: true })
|
|
613
|
+
_stopSignal?.addEventListener('abort', stop, { once: true })
|
|
599
614
|
|
|
600
615
|
const maybeFinishInitialReady = () => {
|
|
601
616
|
if (initialPending.size === 0) finishReady()
|
|
@@ -630,7 +645,7 @@ export class RelayPool {
|
|
|
630
645
|
// Schedule teardown when the wall clock reaches filter.until
|
|
631
646
|
if (filterUntil !== null) {
|
|
632
647
|
const msUntil = filterUntil * 1000 - Date.now()
|
|
633
|
-
untilTimer = maybeUnref(setTimeout(teardown, Math.max(0, msUntil)))
|
|
648
|
+
untilTimer = maybeUnref(setTimeout(() => teardown(true), Math.max(0, msUntil)))
|
|
634
649
|
}
|
|
635
650
|
|
|
636
651
|
// Runs a reconnect gap fill for a single relay and returns a promise that resolves
|
|
@@ -641,11 +656,12 @@ export class RelayPool {
|
|
|
641
656
|
const gapGen = _gapEventsGenerator(gapFilter, [url], {
|
|
642
657
|
timeout: timeoutForReconnectGap,
|
|
643
658
|
timeoutAfterFirstEose: timeoutAfterFirstReconnectGapEose,
|
|
644
|
-
signal
|
|
659
|
+
signal,
|
|
660
|
+
_stopSignal: gapAc.signal
|
|
645
661
|
})
|
|
646
662
|
return (async () => {
|
|
647
663
|
for await (const item of gapGen) {
|
|
648
|
-
if (item?.type === 'event') pushEvent(item.event, url)
|
|
664
|
+
if (item?.type === 'event') pushEvent(item.event, url, true)
|
|
649
665
|
}
|
|
650
666
|
})().catch(err => {
|
|
651
667
|
if (!isDone) console.error(`Reconnect gap fill error for ${url}:`, err)
|
|
@@ -673,7 +689,7 @@ export class RelayPool {
|
|
|
673
689
|
onevent: (event) => {
|
|
674
690
|
// A limit:0 relay may still send retained events before EOSE. Do not
|
|
675
691
|
// expose them from a strictly-live stream.
|
|
676
|
-
if (!liveEose) return
|
|
692
|
+
if (isDone || liveSubs.get(url) !== liveSub || !liveEose) return
|
|
677
693
|
if (liveBuffer) liveBuffer.push(event)
|
|
678
694
|
else pushEvent(event, url)
|
|
679
695
|
},
|
|
@@ -700,12 +716,14 @@ export class RelayPool {
|
|
|
700
716
|
liveSubs.set(url, liveSub)
|
|
701
717
|
|
|
702
718
|
if (gapSince !== null && gapSince > 0) {
|
|
703
|
-
runReconnectGapFill(url, gapSince, now).
|
|
704
|
-
if (isDone) return
|
|
719
|
+
const task = runReconnectGapFill(url, gapSince, now).finally(() => {
|
|
705
720
|
const buf = liveBuffer
|
|
706
721
|
liveBuffer = null
|
|
707
|
-
for (const event of buf) pushEvent(event, url)
|
|
722
|
+
for (const event of buf) pushEvent(event, url, true)
|
|
723
|
+
gapTasks.delete(task)
|
|
724
|
+
p.resolve()
|
|
708
725
|
})
|
|
726
|
+
gapTasks.add(task)
|
|
709
727
|
}
|
|
710
728
|
}).catch(err => {
|
|
711
729
|
readyRelays.delete(url)
|
|
@@ -729,12 +747,14 @@ export class RelayPool {
|
|
|
729
747
|
|
|
730
748
|
try {
|
|
731
749
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
732
|
-
while (!isDone || queue.length > 0) {
|
|
750
|
+
while (!isDone || (draining && gapTasks.size > 0) || queue.length > 0) {
|
|
751
|
+
if (signal?.aborted) break
|
|
733
752
|
if (queue.length > 0) yield queue.shift()
|
|
734
|
-
else await p.promise
|
|
753
|
+
else { await p.promise; p = Promise.withResolvers() }
|
|
735
754
|
}
|
|
736
755
|
} finally {
|
|
737
|
-
signal?.removeEventListener('abort',
|
|
756
|
+
signal?.removeEventListener('abort', abort)
|
|
757
|
+
_stopSignal?.removeEventListener('abort', stop)
|
|
738
758
|
for (const url of urls) this.#decrementLiveSub(url)
|
|
739
759
|
teardown()
|
|
740
760
|
}
|
|
@@ -752,17 +772,24 @@ export class RelayPool {
|
|
|
752
772
|
// relays when null.
|
|
753
773
|
//
|
|
754
774
|
// All underlying generators are injectable for testing.
|
|
755
|
-
|
|
775
|
+
getEventsFeedGenerator (filter, relays, options = {}) {
|
|
776
|
+
return drainableStream(options => this.#getEventsFeedGenerator(filter, relays, options), options)
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
async * #getEventsFeedGenerator (filter, relays, {
|
|
756
780
|
signal,
|
|
781
|
+
_stopSignal,
|
|
757
782
|
live = true,
|
|
758
783
|
timeout = 5000,
|
|
759
784
|
timeoutAfterFirstEose = 500,
|
|
760
785
|
_liveGenerator = (...args) => this.getLiveEventsGenerator(...args),
|
|
761
786
|
_eventsGenerator = (...args) => this.getEventsGenerator(...args)
|
|
762
787
|
} = {}) {
|
|
788
|
+
if (signal.aborted || _stopSignal.aborted) return
|
|
763
789
|
if (!live) {
|
|
764
|
-
const gen = _eventsGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal })
|
|
790
|
+
const gen = _eventsGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal, _stopSignal })
|
|
765
791
|
for await (const item of gen) {
|
|
792
|
+
if (signal.aborted) return
|
|
766
793
|
if (item?.type === 'event') yield item.event
|
|
767
794
|
}
|
|
768
795
|
return
|
|
@@ -770,7 +797,8 @@ export class RelayPool {
|
|
|
770
797
|
|
|
771
798
|
// limit:0 means "no stored events, live only" — skip the initial fetch.
|
|
772
799
|
if (filter.limit === 0) {
|
|
773
|
-
for await (const event of _liveGenerator(filter, relays, { signal })) {
|
|
800
|
+
for await (const event of _liveGenerator(filter, relays, { signal, _stopSignal })) {
|
|
801
|
+
if (signal.aborted) return
|
|
774
802
|
yield event
|
|
775
803
|
}
|
|
776
804
|
return
|
|
@@ -780,7 +808,7 @@ export class RelayPool {
|
|
|
780
808
|
// buffering incoming events before we query stored ones.
|
|
781
809
|
// Relays always send stored matching events before EOSE (unless limit:0),
|
|
782
810
|
// so the initial fetch + buffering is always needed.
|
|
783
|
-
const liveGen = _liveGenerator(filter, relays, { signal })
|
|
811
|
+
const liveGen = _liveGenerator(filter, relays, { signal, _stopSignal })
|
|
784
812
|
const liveBuffer = []
|
|
785
813
|
let liveDone = false
|
|
786
814
|
let liveWake = Promise.withResolvers()
|
|
@@ -788,6 +816,7 @@ export class RelayPool {
|
|
|
788
816
|
const bgLoop = (async () => {
|
|
789
817
|
try {
|
|
790
818
|
for await (const event of liveGen) {
|
|
819
|
+
if (signal.aborted) break
|
|
791
820
|
liveBuffer.push(event)
|
|
792
821
|
liveWake.resolve()
|
|
793
822
|
liveWake = Promise.withResolvers()
|
|
@@ -800,10 +829,11 @@ export class RelayPool {
|
|
|
800
829
|
|
|
801
830
|
try {
|
|
802
831
|
// Yield stored events from the initial one-shot fetch
|
|
803
|
-
const fetchGen = _eventsGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal })
|
|
832
|
+
const fetchGen = _eventsGenerator(filter, relays, { timeout, timeoutAfterFirstEose, signal, _stopSignal })
|
|
804
833
|
|
|
805
834
|
const seenIds = new Set()
|
|
806
835
|
for await (const item of fetchGen) {
|
|
836
|
+
if (signal.aborted) return
|
|
807
837
|
if (item?.type === 'event' && !seenIds.has(item.event.id)) {
|
|
808
838
|
seenIds.add(item.event.id)
|
|
809
839
|
yield item.event
|
|
@@ -813,6 +843,7 @@ export class RelayPool {
|
|
|
813
843
|
// Flush buffered live events that arrived during the initial fetch, deduping
|
|
814
844
|
// against stored ones (overlap is possible around the fetch's until boundary)
|
|
815
845
|
while (liveBuffer.length > 0) {
|
|
846
|
+
if (signal.aborted) return
|
|
816
847
|
const event = liveBuffer.shift()
|
|
817
848
|
if (!seenIds.has(event.id)) {
|
|
818
849
|
seenIds.add(event.id)
|
|
@@ -823,11 +854,14 @@ export class RelayPool {
|
|
|
823
854
|
// Yield subsequent live events directly — no more overlap with stored events
|
|
824
855
|
// eslint-disable-next-line no-unmodified-loop-condition
|
|
825
856
|
while (!liveDone || liveBuffer.length > 0) {
|
|
826
|
-
while (liveBuffer.length > 0)
|
|
857
|
+
while (liveBuffer.length > 0) {
|
|
858
|
+
if (signal.aborted) return
|
|
859
|
+
yield liveBuffer.shift()
|
|
860
|
+
}
|
|
827
861
|
if (!liveDone) await liveWake.promise
|
|
828
862
|
}
|
|
829
863
|
} finally {
|
|
830
|
-
liveGen.return()
|
|
864
|
+
await liveGen.return()
|
|
831
865
|
await bgLoop
|
|
832
866
|
}
|
|
833
867
|
}
|