libp2r2p 0.10.23 → 0.10.25
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/network/README.md +21 -9
- package/network/index.js +86 -23
- package/package.json +1 -1
- package/relay/services/relay-pool.js +12 -2
package/network/README.md
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
# Connectivity
|
|
2
2
|
|
|
3
|
-
`isOnline({ signal } = {})` returns a boolean after probing
|
|
4
|
-
endpoints, or immediately returns `false` when the browser
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`isOnline({ signal, strict = false } = {})` returns a boolean after probing
|
|
4
|
+
public connectivity endpoints, or immediately returns `false` when the browser
|
|
5
|
+
reports offline. The first candidate probes alone with a 2.5-second timeout; if
|
|
6
|
+
it is still pending after 1 second, the remaining candidates start in parallel
|
|
7
|
+
with four-second timeouts and the first success wins, keeping the worst case near
|
|
8
|
+
five seconds instead of serializing every timeout. Concurrent calls without a
|
|
9
|
+
signal share one check per mode. A supplied abort signal cancels the caller's
|
|
10
|
+
independent check immediately.
|
|
11
|
+
|
|
12
|
+
By default a probe only proves the endpoint is reachable (`no-cors`), so HTTP
|
|
13
|
+
errors and captive portals still resolve. With `strict: true`, probes use
|
|
14
|
+
CORS-enabled endpoints and require `response.ok` plus the expected body marker,
|
|
15
|
+
which rejects captive portals and error pages. `createConnectivityMonitor({
|
|
16
|
+
strict })` and `onOnline(handler, { strict })` select the same mode; shared
|
|
17
|
+
checks, monitors and listeners are kept per mode.
|
|
7
18
|
|
|
8
19
|
`onOnline(handler)` returns an idempotent unsubscribe function. It notifies
|
|
9
20
|
asynchronously after confirmed initial connectivity and each detected recovery.
|
|
10
21
|
The native `online` event only triggers a check; it is not sufficient evidence.
|
|
11
|
-
Listeners share a monitor with retries at 5, 15, 30, then 60 seconds
|
|
12
|
-
checks on focus/visibility/network changes, and a 60-second interval
|
|
13
|
-
Unsubscribing the last listener removes timers/listeners and aborts
|
|
14
|
-
Handler failures are isolated and logged.
|
|
15
|
-
creates an independent monitor with an
|
|
22
|
+
Listeners of a mode share a monitor with retries at 5, 15, 30, then 60 seconds
|
|
23
|
+
(20% jitter), checks on focus/visibility/network changes, and a 60-second interval
|
|
24
|
+
while online. Unsubscribing the last listener removes timers/listeners and aborts
|
|
25
|
+
its probe. Handler failures are isolated and logged.
|
|
26
|
+
`createConnectivityMonitor(options)` creates an independent monitor with an
|
|
27
|
+
optional custom `check({ signal })` and `strict` default.
|
|
16
28
|
|
|
17
29
|
Successful probes do not guarantee a particular relay or HTTP server is reachable.
|
|
18
30
|
Handle request failures, cancellations, and timeouts separately. Browsers can
|
package/network/index.js
CHANGED
|
@@ -7,28 +7,74 @@ const CONNECTIVITY_PROBE_URLS = [
|
|
|
7
7
|
{ url: 'https://captive.apple.com/hotspot-detect.html' },
|
|
8
8
|
{ method: 'GET', url: 'https://connectivity-check.ubuntu.com' }
|
|
9
9
|
]
|
|
10
|
-
let
|
|
10
|
+
// CORS-enabled endpoints let the strict mode validate the response instead of
|
|
11
|
+
// merely proving the host is reachable.
|
|
12
|
+
const STRICT_CONNECTIVITY_PROBE_URLS = [
|
|
13
|
+
{ url: 'https://captive.apple.com/hotspot-detect.html', method: 'GET', marker: 'Success' },
|
|
14
|
+
{ url: 'https://1.1.1.1/cdn-cgi/trace', method: 'GET', marker: 'ip=' },
|
|
15
|
+
{ url: 'https://cloudflare.com/cdn-cgi/trace', method: 'GET', marker: 'ip=' }
|
|
16
|
+
]
|
|
17
|
+
const FIRST_PROBE_TIMEOUT_MS = 2500
|
|
18
|
+
const HEDGE_DELAY_MS = 1000
|
|
19
|
+
const REMAINING_PROBE_TIMEOUT_MS = 4000
|
|
20
|
+
const sharedChecks = new Map()
|
|
11
21
|
|
|
12
|
-
// Treat the browser's offline flag as a fast failure; confirm online status with
|
|
13
|
-
|
|
22
|
+
// Treat the browser's offline flag as a fast failure; confirm online status with
|
|
23
|
+
// probes. The default mode only proves the hosts are reachable, while strict mode
|
|
24
|
+
// requires CORS responses with an expected body and rejects captive portals.
|
|
25
|
+
export async function isOnline ({ signal, strict = false } = {}) {
|
|
14
26
|
if (signal?.aborted) throw signal.reason
|
|
15
27
|
if (globalThis.navigator?.onLine === false) return false
|
|
16
|
-
if (signal) return hasInternetConnectivity(signal)
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
if (signal) return hasInternetConnectivity(signal, strict)
|
|
29
|
+
const key = strict ? 'strict' : 'lenient'
|
|
30
|
+
if (!sharedChecks.has(key)) {
|
|
31
|
+
sharedChecks.set(key, hasInternetConnectivity(undefined, strict).finally(() => { sharedChecks.delete(key) }))
|
|
32
|
+
}
|
|
33
|
+
return sharedChecks.get(key)
|
|
19
34
|
}
|
|
20
35
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
// The first candidate answers the common case alone. When it is still pending
|
|
37
|
+
// after HEDGE_DELAY_MS, the remaining candidates race in parallel so one slow or
|
|
38
|
+
// blocked host cannot serialize every timeout.
|
|
39
|
+
async function hasInternetConnectivity (signal, strict) {
|
|
40
|
+
if (signal?.aborted) throw signal.reason
|
|
41
|
+
const candidates = shuffle(strict ? STRICT_CONNECTIVITY_PROBE_URLS : CONNECTIVITY_PROBE_URLS)
|
|
42
|
+
const [first, ...rest] = candidates
|
|
43
|
+
if (!first) return false
|
|
44
|
+
const controller = new AbortController()
|
|
45
|
+
const onAbort = () => controller.abort(signal?.reason)
|
|
46
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
47
|
+
if (signal?.aborted) onAbort()
|
|
48
|
+
let hedgeTimer
|
|
49
|
+
let onHedgeAbort
|
|
50
|
+
try {
|
|
51
|
+
let startHedge
|
|
52
|
+
const firstAttempt = ping(first, { strict, signal: controller.signal, timeout: FIRST_PROBE_TIMEOUT_MS })
|
|
53
|
+
const hedge = new Promise((resolve, reject) => {
|
|
54
|
+
startHedge = resolve
|
|
55
|
+
hedgeTimer = setTimeout(resolve, HEDGE_DELAY_MS)
|
|
56
|
+
if (signal) {
|
|
57
|
+
onHedgeAbort = () => reject(signal.reason)
|
|
58
|
+
signal.addEventListener('abort', onHedgeAbort, { once: true })
|
|
59
|
+
if (signal.aborted) onHedgeAbort()
|
|
60
|
+
}
|
|
61
|
+
}).then(() => Promise.any(rest.map(candidate => ping(candidate, { strict, signal: controller.signal, timeout: REMAINING_PROBE_TIMEOUT_MS }))))
|
|
62
|
+
// A definitive failure releases the hedge immediately; a slow probe waits
|
|
63
|
+
// for the hedge delay so a working network still costs one request.
|
|
64
|
+
firstAttempt.catch(() => startHedge())
|
|
24
65
|
try {
|
|
25
|
-
await
|
|
66
|
+
await Promise.any([firstAttempt, hedge])
|
|
26
67
|
return true
|
|
27
68
|
} catch {
|
|
28
69
|
if (signal?.aborted) throw signal.reason
|
|
70
|
+
return false
|
|
29
71
|
}
|
|
72
|
+
} finally {
|
|
73
|
+
clearTimeout(hedgeTimer)
|
|
74
|
+
if (onHedgeAbort) signal?.removeEventListener('abort', onHedgeAbort)
|
|
75
|
+
controller.abort()
|
|
76
|
+
signal?.removeEventListener('abort', onAbort)
|
|
30
77
|
}
|
|
31
|
-
return false
|
|
32
78
|
}
|
|
33
79
|
|
|
34
80
|
function shuffle (list) {
|
|
@@ -41,7 +87,8 @@ function shuffle (list) {
|
|
|
41
87
|
}
|
|
42
88
|
|
|
43
89
|
// Bound each probe and release its timer and abort listener on every exit path.
|
|
44
|
-
async function ping (
|
|
90
|
+
async function ping (candidate, { strict = false, timeout, signal } = {}) {
|
|
91
|
+
if (signal?.aborted) throw signal.reason
|
|
45
92
|
const controller = new AbortController()
|
|
46
93
|
let timer
|
|
47
94
|
let onAbort
|
|
@@ -58,19 +105,29 @@ async function ping (url, { method = 'HEAD', timeout = 5000, signal } = {}) {
|
|
|
58
105
|
if (signal?.aborted) onAbort()
|
|
59
106
|
})
|
|
60
107
|
try {
|
|
61
|
-
await Promise.race([
|
|
62
|
-
fetch(url, {
|
|
108
|
+
const response = await Promise.race([
|
|
109
|
+
fetch(candidate.url, {
|
|
110
|
+
method: candidate.method ?? (strict ? 'GET' : 'HEAD'),
|
|
111
|
+
mode: strict ? 'cors' : 'no-cors',
|
|
112
|
+
cache: 'no-store',
|
|
113
|
+
redirect: 'follow',
|
|
114
|
+
signal: controller.signal
|
|
115
|
+
}),
|
|
63
116
|
stopped
|
|
64
117
|
])
|
|
118
|
+
if (!strict) return
|
|
119
|
+
if (!response.ok) throw new Error('PING_STATUS')
|
|
120
|
+
if (!(await response.text()).includes(candidate.marker)) throw new Error('PING_BODY')
|
|
65
121
|
} finally {
|
|
66
122
|
clearTimeout(timer)
|
|
67
123
|
signal?.removeEventListener('abort', onAbort)
|
|
68
124
|
}
|
|
69
125
|
}
|
|
70
126
|
|
|
71
|
-
// A monitor owns one probe loop, regardless of how many consumers subscribe.
|
|
127
|
+
// A monitor owns one probe loop per mode, regardless of how many consumers subscribe.
|
|
72
128
|
export function createConnectivityMonitor ({
|
|
73
|
-
check
|
|
129
|
+
check,
|
|
130
|
+
strict = false,
|
|
74
131
|
eventTarget = globalThis.window,
|
|
75
132
|
document = globalThis.document,
|
|
76
133
|
_setTimeout = globalThis.setTimeout,
|
|
@@ -78,7 +135,8 @@ export function createConnectivityMonitor ({
|
|
|
78
135
|
_random = Math.random,
|
|
79
136
|
reportError = error => console.error('Online listener failed', error)
|
|
80
137
|
} = {}) {
|
|
81
|
-
if (typeof check !== 'function') throw new ValidationError('INVALID_CONNECTIVITY_CHECK')
|
|
138
|
+
if (check !== undefined && typeof check !== 'function') throw new ValidationError('INVALID_CONNECTIVITY_CHECK')
|
|
139
|
+
const runCheck = check ?? (options => isOnline({ ...options, strict }))
|
|
82
140
|
const listeners = new Set()
|
|
83
141
|
const setTimer = (...args) => Reflect.apply(_setTimeout, globalThis, args)
|
|
84
142
|
const clearTimer = (...args) => Reflect.apply(_clearTimeout, globalThis, args)
|
|
@@ -106,7 +164,7 @@ export function createConnectivityMonitor ({
|
|
|
106
164
|
current.timer = null
|
|
107
165
|
current.pending = true
|
|
108
166
|
try {
|
|
109
|
-
const online = await
|
|
167
|
+
const online = await runCheck({ signal: current.controller.signal })
|
|
110
168
|
if (session !== current) return
|
|
111
169
|
current.online = online === true && globalThis.navigator?.onLine !== false
|
|
112
170
|
if (current.online) {
|
|
@@ -173,10 +231,15 @@ export function createConnectivityMonitor ({
|
|
|
173
231
|
return { onOnline }
|
|
174
232
|
}
|
|
175
233
|
|
|
176
|
-
|
|
234
|
+
const defaultMonitors = new Map()
|
|
177
235
|
|
|
178
|
-
// Share probes, capped backoff and wake-up listeners
|
|
179
|
-
export function onOnline (handler) {
|
|
180
|
-
|
|
181
|
-
|
|
236
|
+
// Share probes, capped backoff and wake-up listeners per mode in this realm.
|
|
237
|
+
export function onOnline (handler, { strict = false } = {}) {
|
|
238
|
+
const key = strict ? 'strict' : 'lenient'
|
|
239
|
+
let monitor = defaultMonitors.get(key)
|
|
240
|
+
if (!monitor) {
|
|
241
|
+
monitor = createConnectivityMonitor({ strict })
|
|
242
|
+
defaultMonitors.set(key, monitor)
|
|
243
|
+
}
|
|
244
|
+
return monitor.onOnline(handler)
|
|
182
245
|
}
|
package/package.json
CHANGED
|
@@ -136,13 +136,23 @@ export class RelayPool {
|
|
|
136
136
|
#liveSubCounts = new Map() // url -> number of active live subscriptions
|
|
137
137
|
#timeout = 30000 // 30 seconds
|
|
138
138
|
#createRelay
|
|
139
|
+
#WebSocket
|
|
139
140
|
#admission
|
|
140
141
|
|
|
141
|
-
constructor ({ _createRelay
|
|
142
|
-
this.#
|
|
142
|
+
constructor ({ _createRelay, WebSocket: WebSocketImpl, ...capacity } = {}) {
|
|
143
|
+
this.#WebSocket = WebSocketImpl
|
|
144
|
+
this.#createRelay = _createRelay ?? (url => new RelayConnection(url, this.#WebSocket ? { WebSocket: this.#WebSocket } : undefined))
|
|
143
145
|
this.#admission = new ReadAdmission(capacity)
|
|
144
146
|
}
|
|
145
147
|
|
|
148
|
+
// Injects the WebSocket implementation used by new pooled connections (e.g.
|
|
149
|
+
// a launcher-owned relay multiplexer). Existing connections keep the
|
|
150
|
+
// implementation they were created with.
|
|
151
|
+
setWebSocket (WebSocketImpl) {
|
|
152
|
+
if (typeof WebSocketImpl !== 'function') throw new ValidationError('INVALID_WEBSOCKET_IMPLEMENTATION')
|
|
153
|
+
this.#WebSocket = WebSocketImpl
|
|
154
|
+
}
|
|
155
|
+
|
|
146
156
|
#scheduleIdleDisconnect (url) {
|
|
147
157
|
clearTimeout(this.#relayTimeouts.get(url))
|
|
148
158
|
this.#relayTimeouts.set(url, maybeUnref(setTimeout(() => {
|