libp2r2p 0.0.1

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.
Files changed (63) hide show
  1. package/AGENTS.md +20 -0
  2. package/README.md +129 -0
  3. package/base16/index.js +14 -0
  4. package/base64/index.js +29 -0
  5. package/content-key/event/index.js +85 -0
  6. package/content-key/index.js +1 -0
  7. package/content-key/services/iykc-proof.js +129 -0
  8. package/double-dh/index.js +166 -0
  9. package/ecdh/index.js +8 -0
  10. package/idb/index.js +64 -0
  11. package/idb-queue/index.js +793 -0
  12. package/index.js +21 -0
  13. package/key/index.js +139 -0
  14. package/network/index.js +68 -0
  15. package/nip44-v3/index.js +175 -0
  16. package/nip46/constants/index.js +5 -0
  17. package/nip46/helpers/frame.js +51 -0
  18. package/nip46/helpers/url.js +96 -0
  19. package/nip46/index.js +5 -0
  20. package/nip46/services/bunker-signer.js +49 -0
  21. package/nip46/services/client.js +252 -0
  22. package/nip46/services/server-session.js +225 -0
  23. package/nip46/services/transport.js +265 -0
  24. package/package.json +49 -0
  25. package/private-channel/constants/index.js +5 -0
  26. package/private-channel/helpers/chunk-size.js +61 -0
  27. package/private-channel/helpers/chunks.js +282 -0
  28. package/private-channel/helpers/event.js +68 -0
  29. package/private-channel/index.js +942 -0
  30. package/private-channel/services/received-chunks.js +398 -0
  31. package/private-message/index.js +672 -0
  32. package/private-messenger/constants/index.js +1 -0
  33. package/private-messenger/index.js +1431 -0
  34. package/private-messenger/recovery/index.js +316 -0
  35. package/relay/constants/index.js +18 -0
  36. package/relay/helpers/hll.js +62 -0
  37. package/relay/helpers/publish.js +100 -0
  38. package/relay/helpers/routing.js +36 -0
  39. package/relay/helpers/timer.js +4 -0
  40. package/relay/index.js +4 -0
  41. package/relay/services/query.js +224 -0
  42. package/relay/services/relay-connection.js +156 -0
  43. package/relay/services/relay-pool.js +908 -0
  44. package/temporary-storage/index.js +85 -0
  45. package/tests/base16.test.js +19 -0
  46. package/tests/base64.test.js +18 -0
  47. package/tests/content-key-event.test.js +48 -0
  48. package/tests/fixtures/nip44v3-vectors.json +1 -0
  49. package/tests/helpers/test-signer.js +185 -0
  50. package/tests/idb-queue.test.js +153 -0
  51. package/tests/idb.test.js +91 -0
  52. package/tests/nip44-v3.test.js +73 -0
  53. package/tests/nip46.test.js +668 -0
  54. package/tests/private-channel.test.js +1899 -0
  55. package/tests/private-message.test.js +460 -0
  56. package/tests/private-messenger.test.js +1715 -0
  57. package/tests/queries.test.js +101 -0
  58. package/tests/queue-parity.test.js +105 -0
  59. package/tests/relay-hll.test.js +32 -0
  60. package/tests/relay-pool.test.js +2067 -0
  61. package/tests/temporary-storage.test.js +89 -0
  62. package/tests/web-storage-queue.test.js +480 -0
  63. package/web-storage-queue/index.js +652 -0
@@ -0,0 +1,316 @@
1
+ import { getEventHash } from 'nostr-tools'
2
+ import { bytesToBase64, base64ToBytes } from '../../base64/index.js'
3
+ import { ASK_KIND, parseRumorContent } from '../../private-message/index.js'
4
+
5
+ export const SEEDER_PRESENCE_CODE = 'seederPresence_8mj8'
6
+ export const MISSING_MESSAGES_ASK_CODE = 'missingMessages_ask_8mj8'
7
+ export const MISSING_MESSAGES_REPLY_CODE = 'missingMessages_reply_8mj8'
8
+ export const ROUTER_SEED_RECORD_TYPE = 'routerEnvelopeRow_v1'
9
+ export const NYM_CARRIER_SEED_RECORD_TYPE = 'nymCarrier_v1'
10
+
11
+ const DEFAULT_EVENTS_PER_CHUNK = 100
12
+ const encoder = new TextEncoder()
13
+ const decoder = new TextDecoder()
14
+ const HASH_PUBKEY = '0'.repeat(64)
15
+
16
+ function nowSeconds () {
17
+ return Math.floor(Date.now() / 1000)
18
+ }
19
+
20
+ function isPlainObject (value) {
21
+ return value && typeof value === 'object' && !Array.isArray(value)
22
+ }
23
+
24
+ function parseEventContent (event) {
25
+ return parseRumorContent(event)
26
+ }
27
+
28
+ function splitJsonl (jsonl) {
29
+ return String(jsonl || '').split('\n').filter(Boolean)
30
+ }
31
+
32
+ function decodeJsonl (content) {
33
+ try { return decoder.decode(base64ToBytes(content || '')) } catch { return '' }
34
+ }
35
+
36
+ function encodeJsonlRows (...rows) {
37
+ return bytesToBase64(encoder.encode(rows.map(row => String(row).endsWith('\n') ? row : `${row}\n`).join('')))
38
+ }
39
+
40
+ function eventInRange (event, since, until) {
41
+ if (!Number.isFinite(event?.created_at)) return true
42
+ if (since != null && event.created_at < since) return false
43
+ if (until != null && event.created_at > until) return false
44
+ return true
45
+ }
46
+
47
+ function compactRouter (router = {}) {
48
+ return {
49
+ kind: router.kind,
50
+ pubkey: router.pubkey,
51
+ created_at: router.created_at,
52
+ tags: (router.tags || []).filter(tag => tag[0] !== 'c')
53
+ }
54
+ }
55
+
56
+ function cloneTags (tags) {
57
+ return (tags || []).map(tag => Array.isArray(tag) ? [...tag] : tag)
58
+ }
59
+
60
+ export function compactSeedRouter (router = {}) {
61
+ return {
62
+ ...compactRouter(router),
63
+ content: router.content || ''
64
+ }
65
+ }
66
+
67
+ function parseRouterRow (line) {
68
+ try {
69
+ const record = JSON.parse(line)
70
+ if (!Array.isArray(record) || record.length === 1) return null
71
+ return {
72
+ receiverPubkey: record[0] || '',
73
+ iykcPubkey: record[2] || ''
74
+ }
75
+ } catch {
76
+ return null
77
+ }
78
+ }
79
+
80
+ function parsePayloadRow (line) {
81
+ try {
82
+ const record = JSON.parse(line)
83
+ if (!Array.isArray(record) || record.length !== 1 || typeof record[0] !== 'string') return ''
84
+ return line
85
+ } catch {
86
+ return ''
87
+ }
88
+ }
89
+
90
+ function rowHash (payloadRow, row) {
91
+ return getEventHash({
92
+ kind: 0,
93
+ pubkey: HASH_PUBKEY,
94
+ created_at: 0,
95
+ tags: [],
96
+ content: `${payloadRow}\n${row}`
97
+ })
98
+ }
99
+
100
+ export function compactSeedRouterRows (seed = {}) {
101
+ const router = compactRouter(seed.router)
102
+ const createdAt = router.created_at || seed.outer?.created_at || nowSeconds()
103
+ const rows = []
104
+ const innerEventIdsByRowIndex = seed.innerEventIdsByRowIndex || {}
105
+ const lines = splitJsonl(seed.jsonl || decodeJsonl(seed.router?.content))
106
+ const payloadRow = parsePayloadRow(lines[0])
107
+ if (!payloadRow) return []
108
+ for (let index = 0; index < lines.length; index++) {
109
+ const row = lines[index]
110
+ const parsed = parseRouterRow(row)
111
+ if (!parsed?.receiverPubkey) continue
112
+ const innerEventId = innerEventIdsByRowIndex[index] || innerEventIdsByRowIndex[String(index)] || ''
113
+ rows.push({
114
+ type: 'seed',
115
+ recordType: ROUTER_SEED_RECORD_TYPE,
116
+ router,
117
+ receiverPubkey: parsed.receiverPubkey,
118
+ iykcPubkey: parsed.iykcPubkey,
119
+ innerEventId,
120
+ rowHash: innerEventId ? '' : rowHash(payloadRow, row),
121
+ payloadRow,
122
+ row,
123
+ firstSeenAt: createdAt,
124
+ lastSeenAt: createdAt
125
+ })
126
+ }
127
+ return rows
128
+ }
129
+
130
+ export function routerSeedRowKey (seed = {}) {
131
+ const row = seed.row ? parseRouterRow(seed.row) : null
132
+ const innerEventId = seed.innerEventId || ''
133
+ const fallbackRowHash = seed.rowHash || (seed.payloadRow && seed.row ? rowHash(seed.payloadRow, seed.row) : '')
134
+ return [
135
+ seed.channelPubkey || '',
136
+ seed.receiverPubkey || row?.receiverPubkey || '',
137
+ innerEventId ? `event:${innerEventId}` : `row:${fallbackRowHash}`
138
+ ].join(':')
139
+ }
140
+
141
+ export function compactSeedNymCarriers (carriers = []) {
142
+ return carriers.map(carrier => ({
143
+ id: carrier.id,
144
+ kind: carrier.kind,
145
+ pubkey: carrier.pubkey,
146
+ created_at: carrier.created_at,
147
+ tags: cloneTags(carrier.tags),
148
+ content: carrier.content || '',
149
+ sig: carrier.sig
150
+ }))
151
+ }
152
+
153
+ function routerWithSingleRow (router, payloadRow, row) {
154
+ const compact = compactRouter(router)
155
+ return {
156
+ ...compact,
157
+ tags: compact.tags.concat([['c', '0', '1']]),
158
+ content: encodeJsonlRows(payloadRow, row)
159
+ }
160
+ }
161
+
162
+ function seedRowInRange (seed, since, until) {
163
+ const firstSeenAt = seed.firstSeenAt ?? seed.router?.created_at
164
+ const lastSeenAt = seed.lastSeenAt ?? seed.router?.created_at
165
+ if (!Number.isFinite(firstSeenAt) || !Number.isFinite(lastSeenAt)) return eventInRange(seed.router, since, until)
166
+ if (since != null && lastSeenAt < since) return false
167
+ if (until != null && firstSeenAt > until) return false
168
+ return true
169
+ }
170
+
171
+ function compactRoutersFromSeed (seed, { receiverPubkey, since, until }) {
172
+ if (!seed?.payloadRow || !seed?.row || !seed?.router || !seedRowInRange(seed, since, until)) return []
173
+ if (receiverPubkey && seed.receiverPubkey !== receiverPubkey) return []
174
+ return [{
175
+ recordType: ROUTER_SEED_RECORD_TYPE,
176
+ router: routerWithSingleRow(seed.router, seed.payloadRow, seed.row)
177
+ }]
178
+ }
179
+
180
+ function nymCarrierRecordTime (seed) {
181
+ return seed?.carriers?.reduce((max, carrier) => Math.max(max, carrier.created_at || 0), 0) || 0
182
+ }
183
+
184
+ function compactNymCarriersFromSeed (seed, { since, until }) {
185
+ // eslint-disable-next-line camelcase
186
+ const created_at = nymCarrierRecordTime(seed)
187
+ // eslint-disable-next-line camelcase
188
+ if (!seed?.carriers?.length || !eventInRange({ created_at }, since, until)) return []
189
+ return [{
190
+ recordType: NYM_CARRIER_SEED_RECORD_TYPE,
191
+ carriers: compactSeedNymCarriers(seed.carriers)
192
+ }]
193
+ }
194
+
195
+ function compactRecordsFromSeed (seed, { receiverPubkey, since, until }) {
196
+ if (seed?.recordType === NYM_CARRIER_SEED_RECORD_TYPE) {
197
+ return compactNymCarriersFromSeed(seed, { since, until })
198
+ }
199
+ if (seed?.recordType === ROUTER_SEED_RECORD_TYPE) {
200
+ return compactRoutersFromSeed(seed, { receiverPubkey, since, until })
201
+ }
202
+ return []
203
+ }
204
+
205
+ function backfillRequestRange (question, since, until) {
206
+ const content = parseEventContent({ ...question, kind: ASK_KIND })
207
+ const payload = isPlainObject(content?.payload) ? content.payload : {}
208
+ return {
209
+ since: since ?? payload.since ?? 0,
210
+ until: until ?? payload.until ?? nowSeconds()
211
+ }
212
+ }
213
+
214
+ function eventRecordFromInput (event) {
215
+ return Number.isInteger(event?.kind) ? [event] : []
216
+ }
217
+
218
+ export function createEventReplyPacker ({
219
+ messenger,
220
+ channelPubkey,
221
+ question,
222
+ receiverPubkey = question?.pubkey,
223
+ code,
224
+ payload = {},
225
+ eventsPerChunk = DEFAULT_EVENTS_PER_CHUNK,
226
+ recordsFromInput = eventRecordFromInput,
227
+ sendEmptyReply = false
228
+ }) {
229
+ if (!messenger?.reply) throw new Error('MESSENGER_REQUIRED')
230
+ if (!question?.id) throw new Error('QUESTION_REQUIRED')
231
+ if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
232
+ if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new Error('INVALID_EVENTS_PER_CHUNK')
233
+
234
+ let chunk = ''
235
+ let chunkEvents = 0
236
+ let index = 0
237
+ let finalized = false
238
+ let published = false
239
+
240
+ async function publish (isLast) {
241
+ const jsonl = chunk
242
+ chunk = ''
243
+ chunkEvents = 0
244
+ published = true
245
+ await messenger.reply({
246
+ channelPubkey,
247
+ question,
248
+ receiverPubkey,
249
+ code,
250
+ payload: {
251
+ ...payload,
252
+ index: index++,
253
+ isLast,
254
+ jsonl
255
+ }
256
+ })
257
+ }
258
+
259
+ async function appendRecord (record, { flush = true } = {}) {
260
+ chunk += `${JSON.stringify(record)}\n`
261
+ chunkEvents++
262
+ if (flush && chunkEvents >= eventsPerChunk) await publish(false)
263
+ }
264
+
265
+ async function appendRecords (records, { final = false } = {}) {
266
+ for (let i = 0; i < records.length; i++) {
267
+ const isLast = final && i === records.length - 1
268
+ await appendRecord(records[i], { flush: !isLast })
269
+ }
270
+ }
271
+
272
+ async function update (input) {
273
+ if (finalized) throw new Error('PACKER_FINALIZED')
274
+ await appendRecords(await recordsFromInput(input))
275
+ }
276
+
277
+ async function finalize (input) {
278
+ if (finalized) return
279
+ if (input != null) await appendRecords(await recordsFromInput(input), { final: true })
280
+ finalized = true
281
+ if (!chunk && !sendEmptyReply && !published) return
282
+ await publish(true)
283
+ }
284
+
285
+ return {
286
+ update,
287
+ finalize
288
+ }
289
+ }
290
+
291
+ export function createMissingMessageReplyPacker ({
292
+ messenger,
293
+ channelPubkey,
294
+ question,
295
+ receiverPubkey = question?.pubkey,
296
+ since,
297
+ until,
298
+ eventsPerChunk = DEFAULT_EVENTS_PER_CHUNK
299
+ }) {
300
+ if (!messenger?.reply) throw new Error('MESSENGER_REQUIRED')
301
+ if (!question?.id) throw new Error('QUESTION_REQUIRED')
302
+ if (!receiverPubkey) throw new Error('RECEIVER_PUBKEY_REQUIRED')
303
+ if (!Number.isSafeInteger(eventsPerChunk) || eventsPerChunk < 1) throw new Error('INVALID_EVENTS_PER_CHUNK')
304
+
305
+ const range = backfillRequestRange(question, since, until)
306
+ return createEventReplyPacker({
307
+ messenger,
308
+ channelPubkey,
309
+ question,
310
+ receiverPubkey,
311
+ code: MISSING_MESSAGES_REPLY_CODE,
312
+ payload: { since: range.since, until: range.until },
313
+ eventsPerChunk,
314
+ recordsFromInput: seed => compactRecordsFromSeed(seed, { receiverPubkey, since: range.since, until: range.until })
315
+ })
316
+ }
@@ -0,0 +1,18 @@
1
+ // Used only to discover users' NIP-65 relay lists (kind:10002).
2
+ export const seedRelays = [
3
+ 'wss://relay.44billion.net',
4
+ 'wss://purplepag.es',
5
+ 'wss://user.kindpag.es',
6
+ 'wss://relay.nos.social',
7
+ 'wss://nostr.land',
8
+ 'wss://indexer.coracle.social'
9
+ ]
10
+
11
+ // Fallback write-accepting relays. Used as the initial write/read-relay set for
12
+ // new accounts and as a fallback when we cannot resolve a user's own relays.
13
+ export const freeRelays = [
14
+ 'wss://relay.44billion.net',
15
+ 'wss://nos.lol',
16
+ 'wss://relay.primal.net',
17
+ 'wss://relay.damus.io'
18
+ ]
@@ -0,0 +1,62 @@
1
+ const REGISTER_COUNT = 256
2
+ const HLL_HEX_LENGTH = REGISTER_COUNT * 2
3
+
4
+ function assertRegisters (registers) {
5
+ if (!(registers instanceof Uint8Array) || registers.length !== REGISTER_COUNT) {
6
+ throw new Error('INVALID_HLL_REGISTERS')
7
+ }
8
+ }
9
+
10
+ // NIP-45 serializes one uint8 register for each of the 256 HLL buckets.
11
+ export function decodeHll (value) {
12
+ if (typeof value !== 'string' || value.length !== HLL_HEX_LENGTH || !/^[0-9a-fA-F]+$/.test(value)) {
13
+ return null
14
+ }
15
+
16
+ const registers = new Uint8Array(REGISTER_COUNT)
17
+ for (let index = 0; index < REGISTER_COUNT; index++) {
18
+ registers[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16)
19
+ }
20
+ return registers
21
+ }
22
+
23
+ export function encodeHll (registers) {
24
+ assertRegisters(registers)
25
+
26
+ let value = ''
27
+ for (const register of registers) value += register.toString(16).padStart(2, '0')
28
+ return value
29
+ }
30
+
31
+ // Combining independent relay sketches keeps the largest observation per bucket.
32
+ export function mergeHll (target, source) {
33
+ assertRegisters(target)
34
+ assertRegisters(source)
35
+
36
+ for (let index = 0; index < REGISTER_COUNT; index++) {
37
+ if (source[index] > target[index]) target[index] = source[index]
38
+ }
39
+ return target
40
+ }
41
+
42
+ // Small sketches use linear counting; larger ones use the standard HLL estimate.
43
+ export function estimateHllCount (registers) {
44
+ assertRegisters(registers)
45
+
46
+ let zeroes = 0
47
+ let sum = 0
48
+ for (const register of registers) {
49
+ if (register === 0) zeroes++
50
+ sum += 1 / 2 ** register
51
+ }
52
+
53
+ if (zeroes > 0) {
54
+ const linearCount = REGISTER_COUNT * Math.log(REGISTER_COUNT / zeroes)
55
+ if (linearCount <= 220) return Math.floor(linearCount)
56
+ if (sum > 0 && (0.7182725932495458 * REGISTER_COUNT * REGISTER_COUNT) / sum <= REGISTER_COUNT * 3) {
57
+ return Math.floor(linearCount)
58
+ }
59
+ }
60
+
61
+ return Math.floor((0.7182725932495458 * REGISTER_COUNT * REGISTER_COUNT) / sum)
62
+ }
@@ -0,0 +1,100 @@
1
+ import { maybeUnref } from './timer.js'
2
+
3
+ function publishTimeoutError () {
4
+ return new Error('PUBLISH_TIMEOUT')
5
+ }
6
+
7
+ // Resolves once any relay accepts the event, all relays reject, an optional
8
+ // early deadline expires, or the full report has already finished.
9
+ export function firstFulfillment (promises, timeout, { fallback } = {}) {
10
+ return new Promise((resolve) => {
11
+ let settled = false
12
+ let rejected = 0
13
+ let timer = null
14
+ const finish = (success) => {
15
+ if (settled) return
16
+ settled = true
17
+ clearTimeout(timer)
18
+ resolve(success)
19
+ }
20
+ if (timeout !== null) timer = maybeUnref(setTimeout(() => finish(false), timeout))
21
+ for (const promise of promises) {
22
+ Promise.resolve(promise).then(
23
+ () => finish(true),
24
+ () => {
25
+ rejected++
26
+ if (rejected === promises.length) finish(false)
27
+ }
28
+ )
29
+ }
30
+ if (fallback) Promise.resolve(fallback).then(finish, () => finish(false))
31
+ })
32
+ }
33
+
34
+ // Gives every relay one terminal result under a single operation-wide deadline.
35
+ // timeout() also lets the early-return path close every pending report at once.
36
+ export function createPublishSettlements (promises, timeout, { onSettled } = {}) {
37
+ const settlements = new Array(promises.length)
38
+ let remaining = promises.length
39
+ let timer = null
40
+ let isFinished = false
41
+ let resolve
42
+
43
+ const promise = new Promise(nextResolve => { resolve = nextResolve })
44
+ const finish = () => {
45
+ if (isFinished) return
46
+ isFinished = true
47
+ clearTimeout(timer)
48
+ resolve(settlements)
49
+ }
50
+
51
+ const settle = (index, settlement) => {
52
+ if (settlements[index]) return
53
+ settlements[index] = settlement
54
+ onSettled?.(settlement, index)
55
+ remaining--
56
+ if (remaining === 0) finish()
57
+ }
58
+
59
+ const timeoutPending = () => {
60
+ if (isFinished) return
61
+ for (let index = 0; index < settlements.length; index++) {
62
+ settle(index, { status: 'rejected', outcome: 'timed-out', reason: publishTimeoutError() })
63
+ }
64
+ }
65
+
66
+ if (remaining === 0) finish()
67
+ else {
68
+ if (timeout !== null) timer = maybeUnref(setTimeout(timeoutPending, timeout))
69
+ promises.forEach((promise, index) => {
70
+ Promise.resolve(promise).then(
71
+ value => settle(index, { status: 'fulfilled', value }),
72
+ reason => settle(index, { status: 'rejected', outcome: 'failed', reason })
73
+ )
74
+ })
75
+ }
76
+
77
+ return { promise, timeout: timeoutPending }
78
+ }
79
+
80
+ // Turns ordered relay settlements into a stable, caller-facing report. Failed
81
+ // relays retain their Error while optional successful URLs make acknowledgements visible.
82
+ export function publishSummary (settlements, relays, { result, includeSucceededRelays = false } = {}) {
83
+ const succeededRelays = []
84
+ const errors = []
85
+
86
+ settlements.forEach((settlement, index) => {
87
+ if (settlement.status === 'fulfilled') succeededRelays.push(relays[index])
88
+ else errors.push({ relay: relays[index], reason: settlement.reason })
89
+ })
90
+
91
+ const summary = {
92
+ success: succeededRelays.length > 0,
93
+ total: relays.length,
94
+ fulfilled: succeededRelays.length,
95
+ errors
96
+ }
97
+ if (result !== undefined) summary.result = result
98
+ if (includeSucceededRelays) summary.succeededRelays = succeededRelays
99
+ return summary
100
+ }
@@ -0,0 +1,36 @@
1
+ import { freeRelays } from '../constants/index.js'
2
+
3
+ const DEFAULT_RELAYS_PER_PUBKEY = 2
4
+
5
+ // Given pubkeys and their relay mappings, picks the minimum set of relays
6
+ // that covers all pubkeys (up to maxPerPubkey relays each), preferring
7
+ // relays shared by more pubkeys. Returns Map<relayUrl, pubkey[]>.
8
+ export function pickRelaysForPubkeys (pubkeys, relaysByPubkey, { maxPerPubkey = DEFAULT_RELAYS_PER_PUBKEY, relayType = 'write' } = {}) {
9
+ const type = relayType === 'read' ? 'read' : 'write'
10
+ const pkToPossibleRelays = new Map()
11
+ for (const pk of pubkeys) {
12
+ const relays = relaysByPubkey[pk]?.[type] || []
13
+ pkToPossibleRelays.set(pk, new Set(relays.length ? relays : freeRelays.slice(0, DEFAULT_RELAYS_PER_PUBKEY)))
14
+ }
15
+
16
+ const relayCounts = new Map()
17
+ for (const relays of pkToPossibleRelays.values()) {
18
+ for (const relay of relays) relayCounts.set(relay, (relayCounts.get(relay) || 0) + 1)
19
+ }
20
+ const rankedRelays = [...relayCounts.keys()].sort((a, b) => relayCounts.get(b) - relayCounts.get(a))
21
+
22
+ const relayToAuthors = new Map()
23
+ for (const pk of pubkeys) {
24
+ const possibleRelays = pkToPossibleRelays.get(pk)
25
+ let assigned = 0
26
+ for (const relay of rankedRelays) {
27
+ if (assigned >= maxPerPubkey) break
28
+ if (!possibleRelays.has(relay)) continue
29
+ if (!relayToAuthors.has(relay)) relayToAuthors.set(relay, [])
30
+ relayToAuthors.get(relay).push(pk)
31
+ assigned++
32
+ }
33
+ }
34
+
35
+ return relayToAuthors
36
+ }
@@ -0,0 +1,4 @@
1
+ export function maybeUnref (timer) {
2
+ timer?.unref?.()
3
+ return timer
4
+ }
package/relay/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { freeRelays, seedRelays } from './constants/index.js'
2
+ export { pickRelaysForPubkeys } from './helpers/routing.js'
3
+ export { RelayPool, relayPool } from './services/relay-pool.js'
4
+ export { getRelaysByPubkey, subscribeRelayListUpdates } from './services/query.js'