libp2r2p 0.10.23 → 0.10.24

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 CHANGED
@@ -1,18 +1,30 @@
1
1
  # Connectivity
2
2
 
3
- `isOnline({ signal } = {})` returns a boolean after probing public connectivity
4
- endpoints, or immediately returns `false` when the browser reports offline.
5
- Concurrent calls without a signal share one check. A supplied abort signal
6
- cancels the caller's independent check. Each endpoint has a five-second timeout.
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 (20% jitter),
12
- checks on focus/visibility/network changes, and a 60-second interval while online.
13
- Unsubscribing the last listener removes timers/listeners and aborts its probe.
14
- Handler failures are isolated and logged. `createConnectivityMonitor(options)`
15
- creates an independent monitor with an optional custom `check({ signal })`.
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 sharedCheck
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 a probe.
13
- export async function isOnline ({ signal } = {}) {
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
- sharedCheck ??= hasInternetConnectivity().finally(() => { sharedCheck = null })
18
- return sharedCheck
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
- async function hasInternetConnectivity (signal) {
22
- for (const candidate of shuffle(CONNECTIVITY_PROBE_URLS)) {
23
- if (signal?.aborted) throw signal.reason
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 ping(candidate.url, { method: candidate.method, signal })
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 (url, { method = 'HEAD', timeout = 5000, signal } = {}) {
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, { method, mode: 'no-cors', cache: 'no-store', redirect: 'follow', signal: controller.signal }),
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 = isOnline,
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 check({ signal: current.controller.signal })
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
- let defaultMonitor
234
+ const defaultMonitors = new Map()
177
235
 
178
- // Share probes, capped backoff and wake-up listeners across callers in this realm.
179
- export function onOnline (handler) {
180
- defaultMonitor ??= createConnectivityMonitor()
181
- return defaultMonitor.onOnline(handler)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.10.23",
3
+ "version": "0.10.24",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",