libp2r2p 0.10.2 → 0.10.4
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 +6 -0
- package/nip46/services/client.js +3 -1
- package/nip46/services/transport.js +12 -9
- package/package.json +1 -1
- package/private-messenger/index.js +66 -15
package/README.md
CHANGED
|
@@ -278,6 +278,12 @@ NIP-44 v2 uses the interoperable `nip44-v2` salt by default. A custom UTF-8
|
|
|
278
278
|
salt of at most 32 bytes may be passed to `getConversationKey()`, but messages
|
|
279
279
|
derived with it are not interoperable with standard NIP-44 implementations.
|
|
280
280
|
|
|
281
|
+
NIP-46 clients and bunker signers use a 30-second operation timeout by
|
|
282
|
+
default. Set `timeout` in the `Nip46Client`/`BunkerSigner` constructor to
|
|
283
|
+
choose another default, override it for an individual `connect()` or RPC, or
|
|
284
|
+
pass `timeout: null` explicitly when an operation is intentionally allowed to
|
|
285
|
+
wait indefinitely.
|
|
286
|
+
|
|
281
287
|
Nostr Web Tokens are available from `libp2r2p/nwt`. Creation returns a signed
|
|
282
288
|
kind `27519` event, while transport encoding is kept separate:
|
|
283
289
|
|
package/nip46/services/client.js
CHANGED
|
@@ -129,7 +129,9 @@ export class Nip46Client {
|
|
|
129
129
|
get pointer () { return { ...this.#pointer, relays: [...this.#pointer.relays] } }
|
|
130
130
|
|
|
131
131
|
// Connects and immediately asks the remote signer for its preferred relays.
|
|
132
|
-
|
|
132
|
+
// An omitted timeout inherits the configurable constructor timeout; null
|
|
133
|
+
// explicitly disables the response deadline.
|
|
134
|
+
async connect ({ requestedPermissions = [], clientMetadata, timeout, signal } = {}) {
|
|
133
135
|
const permissions = Array.isArray(requestedPermissions)
|
|
134
136
|
? requestedPermissions.filter(permission => typeof permission === 'string' && permission).join(',')
|
|
135
137
|
: ''
|
|
@@ -59,7 +59,7 @@ export class Nip46Transport {
|
|
|
59
59
|
#secretKey
|
|
60
60
|
#pubkey
|
|
61
61
|
#relayPool
|
|
62
|
-
#
|
|
62
|
+
#operationTimeout
|
|
63
63
|
#timeoutAfterFirstEose
|
|
64
64
|
#onError
|
|
65
65
|
#contexts = new Set()
|
|
@@ -80,7 +80,7 @@ export class Nip46Transport {
|
|
|
80
80
|
this.#secretKey = secretKey
|
|
81
81
|
this.#pubkey = getPublicKey(secretKey)
|
|
82
82
|
this.#relayPool = relayPool
|
|
83
|
-
this.#
|
|
83
|
+
this.#operationTimeout = networkTimeout
|
|
84
84
|
this.#timeoutAfterFirstEose = timeoutAfterFirstEose
|
|
85
85
|
this.#onError = onError
|
|
86
86
|
}
|
|
@@ -111,7 +111,7 @@ export class Nip46Transport {
|
|
|
111
111
|
return context
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async awaitContextReady (context, { timeout = this.#
|
|
114
|
+
async awaitContextReady (context, { timeout = this.#operationTimeout, signal } = {}) {
|
|
115
115
|
const report = await waitForNip46(context.stream.ready, {
|
|
116
116
|
timeout,
|
|
117
117
|
signal,
|
|
@@ -159,11 +159,14 @@ export class Nip46Transport {
|
|
|
159
159
|
return true
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
async sendRequest (peerPubkey, method, params = [], {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
async sendRequest (peerPubkey, method, params = [], options = {}) {
|
|
163
|
+
const {
|
|
164
|
+
// Undefined inherits the constructor-wide operation timeout. Passing
|
|
165
|
+
// null explicitly is the opt-out for RPCs that may wait indefinitely.
|
|
166
|
+
timeout = this.#operationTimeout,
|
|
167
|
+
signal,
|
|
168
|
+
extension
|
|
169
|
+
} = options
|
|
167
170
|
if (this.#closed) throw new Error('NIP46_CLOSED')
|
|
168
171
|
if (typeof method !== 'string' || !method) throw new ValidationError('NIP46_METHOD_REQUIRED')
|
|
169
172
|
if (!Array.isArray(params) || !params.every(param => typeof param === 'string')) {
|
|
@@ -229,7 +232,7 @@ export class Nip46Transport {
|
|
|
229
232
|
if (!relays.length) throw new Error('NIP46_NO_READY_RELAYS')
|
|
230
233
|
const event = createNip46Event({ secretKey: this.#secretKey, recipientPubkey: peerPubkey, payload })
|
|
231
234
|
const published = await this.#relayPool.sendEvent(event, relays, {
|
|
232
|
-
timeout: this.#
|
|
235
|
+
timeout: this.#operationTimeout,
|
|
233
236
|
timeoutUntilFirstFulfillment: null
|
|
234
237
|
})
|
|
235
238
|
if (!published.success) {
|
package/package.json
CHANGED
|
@@ -211,6 +211,7 @@ export class PrivateMessenger {
|
|
|
211
211
|
_pickRelaysForPubkeys = pickRelaysForPubkeys,
|
|
212
212
|
_subscribeRelayListUpdates = subscribeRelayListUpdates,
|
|
213
213
|
_setTimeout = globalThis.setTimeout.bind(globalThis),
|
|
214
|
+
_clearTimeout = globalThis.clearTimeout.bind(globalThis),
|
|
214
215
|
_setInterval = globalThis.setInterval.bind(globalThis),
|
|
215
216
|
_clearInterval = globalThis.clearInterval.bind(globalThis),
|
|
216
217
|
_storageSetInterval = globalThis.setInterval.bind(globalThis),
|
|
@@ -241,6 +242,7 @@ export class PrivateMessenger {
|
|
|
241
242
|
this._pickRelaysForPubkeys = _pickRelaysForPubkeys
|
|
242
243
|
this._subscribeRelayListUpdates = _subscribeRelayListUpdates
|
|
243
244
|
this._setTimeout = _setTimeout
|
|
245
|
+
this._clearTimeout = _clearTimeout
|
|
244
246
|
this._setInterval = _setInterval
|
|
245
247
|
this._clearInterval = _clearInterval
|
|
246
248
|
this._storageSetInterval = _storageSetInterval
|
|
@@ -260,6 +262,8 @@ export class PrivateMessenger {
|
|
|
260
262
|
this.stateWriteTail = Promise.resolve()
|
|
261
263
|
this.channels = new Map()
|
|
262
264
|
this.stopByChannel = new Map()
|
|
265
|
+
this.reloadGapTimers = new Map()
|
|
266
|
+
this.watchRevisionByChannel = new Map()
|
|
263
267
|
this.presenceTimers = new Map()
|
|
264
268
|
this.stopRelayListWatcher = null
|
|
265
269
|
this.relayListWatcherPubkey = ''
|
|
@@ -642,13 +646,13 @@ export class PrivateMessenger {
|
|
|
642
646
|
this.lastStorageTouch = Date.now()
|
|
643
647
|
if (updatesStoragePolicy) this.broadcastStoragePolicyChange()
|
|
644
648
|
|
|
645
|
-
await this.unwatch(
|
|
646
|
-
this.channels.
|
|
649
|
+
await this.unwatch(removedPubkeys)
|
|
650
|
+
for (const pubkey of removedPubkeys) this.channels.delete(pubkey)
|
|
647
651
|
for (const channel of nextChannels) this.channels.set(channel.pubkey, channel)
|
|
648
652
|
|
|
649
653
|
await this.cleanupStaleChannels({ storageSnapshot })
|
|
650
654
|
await this.applyRecoveryPolicies(nextChannels)
|
|
651
|
-
await this.watch()
|
|
655
|
+
await this.watch([...nextPubkeys])
|
|
652
656
|
await this.reconcilePresencePublishers()
|
|
653
657
|
if (this.storagePolicyRevision === storageSnapshot.policyRevision) {
|
|
654
658
|
this.storagePolicyNeedsApply = false
|
|
@@ -1023,6 +1027,7 @@ export class PrivateMessenger {
|
|
|
1023
1027
|
this.assertOpen()
|
|
1024
1028
|
}
|
|
1025
1029
|
this.stopByChannel.set(pubkey, stop)
|
|
1030
|
+
this.watchRevisionByChannel.set(pubkey, (this.watchRevisionByChannel.get(pubkey) || 0) + 1)
|
|
1026
1031
|
this.updateChannelState(pubkey, {
|
|
1027
1032
|
lastWatchedAt: nowSeconds(),
|
|
1028
1033
|
mode: channel.mode,
|
|
@@ -1048,6 +1053,8 @@ export class PrivateMessenger {
|
|
|
1048
1053
|
const channelPubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.stopByChannel.keys()]
|
|
1049
1054
|
const closing = []
|
|
1050
1055
|
for (const pubkey of channelPubkeys) {
|
|
1056
|
+
this.cancelReloadGap(pubkey)
|
|
1057
|
+
this.watchRevisionByChannel.set(pubkey, (this.watchRevisionByChannel.get(pubkey) || 0) + 1)
|
|
1051
1058
|
const close = this.stopByChannel.get(pubkey)?.()
|
|
1052
1059
|
if (close && typeof close.then === 'function') closing.push(close)
|
|
1053
1060
|
this.stopByChannel.delete(pubkey)
|
|
@@ -1570,20 +1577,40 @@ export class PrivateMessenger {
|
|
|
1570
1577
|
}
|
|
1571
1578
|
|
|
1572
1579
|
scheduleReloadGap (pubkey) {
|
|
1580
|
+
this.cancelReloadGap(pubkey)
|
|
1573
1581
|
if (!this.offlineRecoverySecondsFor(pubkey)) return
|
|
1574
1582
|
const current = this.readState().channels[pubkey]
|
|
1575
1583
|
const start = current?.openOfflineStart || current?.lastSeenAt
|
|
1576
1584
|
if (!start) return
|
|
1577
|
-
this.
|
|
1585
|
+
const revision = this.watchRevisionByChannel.get(pubkey) || 0
|
|
1586
|
+
const token = {}
|
|
1587
|
+
const timer = this._setTimeout(async () => {
|
|
1588
|
+
const scheduled = this.reloadGapTimers.get(pubkey)
|
|
1589
|
+
if (scheduled?.token !== token) return
|
|
1590
|
+
this.reloadGapTimers.delete(pubkey)
|
|
1591
|
+
if (this.closePromise || !this.channels.has(pubkey) || !this.stopByChannel.has(pubkey)) return
|
|
1592
|
+
if ((this.watchRevisionByChannel.get(pubkey) || 0) !== revision) return
|
|
1578
1593
|
this.addOfflineRange(pubkey, Math.max(0, start - this.offlineSkewSeconds), nowSeconds())
|
|
1579
1594
|
await this.recoverOfflineRanges([pubkey])
|
|
1580
1595
|
}, this.reloadGapDelayMs)
|
|
1596
|
+
this.reloadGapTimers.set(pubkey, { timer, token, revision })
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
cancelReloadGap (pubkey) {
|
|
1600
|
+
const scheduled = this.reloadGapTimers.get(pubkey)
|
|
1601
|
+
if (!scheduled) return
|
|
1602
|
+
this.reloadGapTimers.delete(pubkey)
|
|
1603
|
+
this._clearTimeout(scheduled.timer)
|
|
1581
1604
|
}
|
|
1582
1605
|
|
|
1583
1606
|
// Browser-offline recovery owns durable gaps. Stop only the child live reads;
|
|
1584
1607
|
// unwatch() would also stop seeder-presence publishing and alter channel state.
|
|
1585
1608
|
#pauseLiveWatches () {
|
|
1586
|
-
for (const stop of this.stopByChannel
|
|
1609
|
+
for (const [pubkey, stop] of this.stopByChannel) {
|
|
1610
|
+
this.cancelReloadGap(pubkey)
|
|
1611
|
+
this.watchRevisionByChannel.set(pubkey, (this.watchRevisionByChannel.get(pubkey) || 0) + 1)
|
|
1612
|
+
stop?.()
|
|
1613
|
+
}
|
|
1587
1614
|
this.stopByChannel.clear()
|
|
1588
1615
|
}
|
|
1589
1616
|
|
|
@@ -1623,32 +1650,49 @@ export class PrivateMessenger {
|
|
|
1623
1650
|
}
|
|
1624
1651
|
|
|
1625
1652
|
async askSeedersForMissingRange (channelPubkey, since, until) {
|
|
1626
|
-
|
|
1627
|
-
|
|
1653
|
+
const { asks } = await this.#askSeedersForMissingRangeAttempt(channelPubkey, since, until)
|
|
1654
|
+
return asks
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
async #askSeedersForMissingRangeAttempt (channelPubkey, since, until) {
|
|
1658
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return { asks: [], failures: [] }
|
|
1659
|
+
if (!this.channels.get(channelPubkey)?.signer) return { asks: [], failures: [] }
|
|
1628
1660
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
1629
|
-
if (!seeders.length || until < since) return []
|
|
1661
|
+
if (!seeders.length || until < since) return { asks: [], failures: [] }
|
|
1630
1662
|
|
|
1631
1663
|
const asks = []
|
|
1664
|
+
const failures = []
|
|
1632
1665
|
for (const seeder of seeders) {
|
|
1633
1666
|
try {
|
|
1634
|
-
|
|
1667
|
+
const ask = await this.ask({
|
|
1635
1668
|
channelPubkey,
|
|
1636
1669
|
receiverPubkey: seeder,
|
|
1637
1670
|
code: MISSING_MESSAGES_ASK_CODE,
|
|
1638
1671
|
payload: { since, until }
|
|
1639
|
-
})
|
|
1672
|
+
})
|
|
1673
|
+
asks.push(ask)
|
|
1674
|
+
const reports = ask?.delivery?.reports
|
|
1675
|
+
if (!Array.isArray(reports) || !reports.length || reports.some(report => report?.success !== true)) {
|
|
1676
|
+
throw new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
|
|
1677
|
+
}
|
|
1640
1678
|
} catch (err) {
|
|
1679
|
+
failures.push({ seeder, error: err })
|
|
1641
1680
|
console.warn('private-messenger seeder recovery ask failed', seeder, err?.message ?? err)
|
|
1642
1681
|
}
|
|
1643
1682
|
}
|
|
1644
|
-
return asks
|
|
1683
|
+
return { asks, failures }
|
|
1645
1684
|
}
|
|
1646
1685
|
|
|
1647
1686
|
async askSeedersForRelayLeftEdge (channelPubkey, range, fetchedEvents) {
|
|
1687
|
+
const { asks } = await this.#askSeedersForRelayLeftEdgeAttempt(channelPubkey, range, fetchedEvents)
|
|
1688
|
+
return asks
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
async #askSeedersForRelayLeftEdgeAttempt (channelPubkey, range, fetchedEvents) {
|
|
1648
1692
|
const oldest = oldestCreatedAt(fetchedEvents)
|
|
1649
1693
|
const until = oldest == null ? range.end : Math.min(range.end, oldest)
|
|
1650
|
-
if (until < range.start) return []
|
|
1651
|
-
return this
|
|
1694
|
+
if (until < range.start) return { asks: [], failures: [] }
|
|
1695
|
+
return this.#askSeedersForMissingRangeAttempt(channelPubkey, range.start, until)
|
|
1652
1696
|
}
|
|
1653
1697
|
|
|
1654
1698
|
async replyWithStoredSeeds (channelPubkey, message) {
|
|
@@ -1759,10 +1803,12 @@ export class PrivateMessenger {
|
|
|
1759
1803
|
const recoverySeconds = this.offlineRecoverySecondsFor(channel)
|
|
1760
1804
|
if (!recoverySeconds) continue
|
|
1761
1805
|
const minStart = now - recoverySeconds
|
|
1806
|
+
const processedRanges = new Set(current.offlineRanges.map(range => `${range.start}:${range.end}`))
|
|
1762
1807
|
|
|
1763
1808
|
const remaining = []
|
|
1764
1809
|
for (const range of current.offlineRanges) {
|
|
1765
1810
|
if (range.end < minStart) continue
|
|
1811
|
+
const watchRevision = this.watchRevisionByChannel.get(pubkey) || 0
|
|
1766
1812
|
try {
|
|
1767
1813
|
const fetchRelays = await this.resolveWatchRelays(channel)
|
|
1768
1814
|
const fetchedEvents = await this._privateChannel.fetch({
|
|
@@ -1786,16 +1832,21 @@ export class PrivateMessenger {
|
|
|
1786
1832
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
1787
1833
|
onError: err => { throw err }
|
|
1788
1834
|
}) || []
|
|
1789
|
-
await this
|
|
1835
|
+
const attempt = await this.#askSeedersForRelayLeftEdgeAttempt(pubkey, range, fetchedEvents)
|
|
1836
|
+
const lifecycleChanged = this.closePromise || !this.channels.has(pubkey) || !this.stopByChannel.has(pubkey) ||
|
|
1837
|
+
(this.watchRevisionByChannel.get(pubkey) || 0) !== watchRevision
|
|
1838
|
+
if (lifecycleChanged || attempt.failures.length) remaining.push(range)
|
|
1790
1839
|
} catch (err) {
|
|
1791
1840
|
this.onError?.(err)
|
|
1792
1841
|
remaining.push(range)
|
|
1793
1842
|
}
|
|
1794
1843
|
}
|
|
1795
1844
|
const fresh = this.readState()
|
|
1845
|
+
const concurrentRanges = (fresh.channels[pubkey]?.offlineRanges || [])
|
|
1846
|
+
.filter(range => !processedRanges.has(`${range.start}:${range.end}`))
|
|
1796
1847
|
fresh.channels[pubkey] = {
|
|
1797
1848
|
...(fresh.channels[pubkey] || {}),
|
|
1798
|
-
offlineRanges: remaining
|
|
1849
|
+
offlineRanges: mergeRanges(concurrentRanges.concat(remaining))
|
|
1799
1850
|
}
|
|
1800
1851
|
this.writeState(fresh)
|
|
1801
1852
|
}
|