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.
- package/AGENTS.md +20 -0
- package/README.md +129 -0
- package/base16/index.js +14 -0
- package/base64/index.js +29 -0
- package/content-key/event/index.js +85 -0
- package/content-key/index.js +1 -0
- package/content-key/services/iykc-proof.js +129 -0
- package/double-dh/index.js +166 -0
- package/ecdh/index.js +8 -0
- package/idb/index.js +64 -0
- package/idb-queue/index.js +793 -0
- package/index.js +21 -0
- package/key/index.js +139 -0
- package/network/index.js +68 -0
- package/nip44-v3/index.js +175 -0
- package/nip46/constants/index.js +5 -0
- package/nip46/helpers/frame.js +51 -0
- package/nip46/helpers/url.js +96 -0
- package/nip46/index.js +5 -0
- package/nip46/services/bunker-signer.js +49 -0
- package/nip46/services/client.js +252 -0
- package/nip46/services/server-session.js +225 -0
- package/nip46/services/transport.js +265 -0
- package/package.json +49 -0
- package/private-channel/constants/index.js +5 -0
- package/private-channel/helpers/chunk-size.js +61 -0
- package/private-channel/helpers/chunks.js +282 -0
- package/private-channel/helpers/event.js +68 -0
- package/private-channel/index.js +942 -0
- package/private-channel/services/received-chunks.js +398 -0
- package/private-message/index.js +672 -0
- package/private-messenger/constants/index.js +1 -0
- package/private-messenger/index.js +1431 -0
- package/private-messenger/recovery/index.js +316 -0
- package/relay/constants/index.js +18 -0
- package/relay/helpers/hll.js +62 -0
- package/relay/helpers/publish.js +100 -0
- package/relay/helpers/routing.js +36 -0
- package/relay/helpers/timer.js +4 -0
- package/relay/index.js +4 -0
- package/relay/services/query.js +224 -0
- package/relay/services/relay-connection.js +156 -0
- package/relay/services/relay-pool.js +908 -0
- package/temporary-storage/index.js +85 -0
- package/tests/base16.test.js +19 -0
- package/tests/base64.test.js +18 -0
- package/tests/content-key-event.test.js +48 -0
- package/tests/fixtures/nip44v3-vectors.json +1 -0
- package/tests/helpers/test-signer.js +185 -0
- package/tests/idb-queue.test.js +153 -0
- package/tests/idb.test.js +91 -0
- package/tests/nip44-v3.test.js +73 -0
- package/tests/nip46.test.js +668 -0
- package/tests/private-channel.test.js +1899 -0
- package/tests/private-message.test.js +460 -0
- package/tests/private-messenger.test.js +1715 -0
- package/tests/queries.test.js +101 -0
- package/tests/queue-parity.test.js +105 -0
- package/tests/relay-hll.test.js +32 -0
- package/tests/relay-pool.test.js +2067 -0
- package/tests/temporary-storage.test.js +89 -0
- package/tests/web-storage-queue.test.js +480 -0
- 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
|
+
}
|
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'
|