libp2r2p 0.10.19 → 0.10.21

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.
@@ -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
@@ -0,0 +1,92 @@
1
+ import { ValidationError } from '../../error/index.js'
2
+ import { maybeUnref } from './timer.js'
3
+
4
+ export function admissionError (code, relay) {
5
+ return Object.assign(new Error(code), { code, relay, phase: 'admission' })
6
+ }
7
+
8
+ // One FIFO per pooled connection. A feed reserves both REQs atomically; its
9
+ // history and live leases can subsequently be released independently.
10
+ export class ReadAdmission {
11
+ constructor ({ maxSubscriptionsPerRelay = 28, maxConcurrentHistoryPerRelay = 2, maxQueuedReadsPerRelay = 256 } = {}) {
12
+ for (const value of [maxSubscriptionsPerRelay, maxConcurrentHistoryPerRelay, maxQueuedReadsPerRelay]) {
13
+ if (!Number.isSafeInteger(value) || value < 1) throw new ValidationError('INVALID_RELAY_READ_CAPACITY')
14
+ }
15
+ Object.assign(this, { maxSubscriptionsPerRelay, maxConcurrentHistoryPerRelay, maxQueuedReadsPerRelay })
16
+ this.states = new Map()
17
+ }
18
+
19
+ acquire (relay, { history = false, live = false, signal, queueTimeout = 30000 } = {}) {
20
+ if (queueTimeout !== null && (!Number.isFinite(queueTimeout) || queueTimeout < 0)) throw new ValidationError('INVALID_RELAY_TIMEOUT')
21
+ const weight = Number(history) + Number(live)
22
+ if (!weight || weight > this.maxSubscriptionsPerRelay) return Promise.reject(admissionError('RELAY_READ_CAPACITY', relay))
23
+ if (signal?.aborted) return Promise.reject(admissionError('RELAY_READ_CANCELLED', relay))
24
+ let state = this.states.get(relay)
25
+ if (!state) {
26
+ state = { active: 0, history: 0, queue: [] }
27
+ this.states.set(relay, state)
28
+ }
29
+ const deferred = Promise.withResolvers()
30
+ let timer
31
+ const job = {
32
+ history, weight, grant: () => {
33
+ cleanup()
34
+ state.active += weight
35
+ state.history += Number(history)
36
+ const leases = {}
37
+ for (const kind of ['history', 'live']) {
38
+ if (!(kind === 'history' ? history : live)) continue
39
+ let released = false
40
+ leases[kind] = {
41
+ release: () => {
42
+ if (released) return
43
+ released = true
44
+ state.active--
45
+ if (kind === 'history') state.history--
46
+ this.drain(relay, state)
47
+ }
48
+ }
49
+ }
50
+ deferred.resolve(leases)
51
+ }, cancel: code => {
52
+ const index = state.queue.indexOf(job)
53
+ if (index < 0) return
54
+ state.queue.splice(index, 1)
55
+ cleanup()
56
+ deferred.reject(admissionError(code, relay))
57
+ this.drain(relay, state)
58
+ }
59
+ }
60
+ const abort = () => job.cancel('RELAY_READ_CANCELLED')
61
+ const cleanup = () => {
62
+ clearTimeout(timer)
63
+ signal?.removeEventListener('abort', abort)
64
+ }
65
+ if (state.queue.length >= this.maxQueuedReadsPerRelay) return Promise.reject(admissionError('RELAY_READ_QUEUE_FULL', relay))
66
+ state.queue.push(job)
67
+ signal?.addEventListener('abort', abort, { once: true })
68
+ if (queueTimeout !== null) timer = maybeUnref(setTimeout(() => job.cancel('RELAY_READ_QUEUE_TIMEOUT'), queueTimeout))
69
+ this.drain(relay, state)
70
+ return deferred.promise
71
+ }
72
+
73
+ drain (relay, state) {
74
+ while (state.queue.length) {
75
+ const job = state.queue[0]
76
+ if (state.active + job.weight > this.maxSubscriptionsPerRelay || (job.history && state.history >= this.maxConcurrentHistoryPerRelay)) break
77
+ state.queue.shift().grant()
78
+ }
79
+ if (!state.active && !state.queue.length && this.states.get(relay) === state) this.states.delete(relay)
80
+ }
81
+
82
+ cancelQueued (relay) {
83
+ const state = this.states.get(relay)
84
+ if (!state) return
85
+ // Remove the entire queue first: cancelling one entry must not admit another.
86
+ const jobs = state.queue.splice(0)
87
+ for (const job of jobs) {
88
+ state.queue.push(job)
89
+ job.cancel('RELAY_DISCONNECTED')
90
+ }
91
+ }
92
+ }