libp2r2p 0.10.10 → 0.10.11
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 +4 -0
- package/network/README.md +20 -0
- package/network/index.js +150 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,10 @@ the transport and discovery surface. The package was born to distribute the
|
|
|
8
8
|
private messenger reference implementation, and it also carries a few Nostr
|
|
9
9
|
power-ups used by that messenger.
|
|
10
10
|
|
|
11
|
+
For remote-work scheduling, see [`libp2r2p/network`](network/README.md):
|
|
12
|
+
`isOnline` probes connectivity and `onOnline` shares recovery monitoring,
|
|
13
|
+
including retries when the browser omits its native `online` event.
|
|
14
|
+
|
|
11
15
|
## Private Messenger
|
|
12
16
|
|
|
13
17
|
The main API is `createPrivateMessenger` from `libp2r2p/private-messenger`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Connectivity
|
|
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.
|
|
7
|
+
|
|
8
|
+
`onOnline(handler)` returns an idempotent unsubscribe function. It notifies
|
|
9
|
+
asynchronously after confirmed initial connectivity and each detected recovery.
|
|
10
|
+
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 })`.
|
|
16
|
+
|
|
17
|
+
Successful probes do not guarantee a particular relay or HTTP server is reachable.
|
|
18
|
+
Handle request failures, cancellations, and timeouts separately. Browsers can
|
|
19
|
+
suspend background tabs, so recovery notification has no wall-clock guarantee.
|
|
20
|
+
Consumers should keep their own work queues but should not duplicate probe loops.
|
package/network/index.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
if (typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean') {
|
|
3
|
-
if (!navigator.onLine) return false
|
|
4
|
-
}
|
|
5
|
-
return hasInternetConnectivity()
|
|
6
|
-
}
|
|
1
|
+
import { ValidationError } from '../error/index.js'
|
|
7
2
|
|
|
3
|
+
const RETRY_DELAYS = [5000, 15000, 30000, 60000]
|
|
8
4
|
const CONNECTIVITY_PROBE_URLS = [
|
|
9
5
|
{ url: 'https://www.gstatic.com/generate_204' },
|
|
10
6
|
{ url: 'https://connectivitycheck.gstatic.com/generate_204' },
|
|
11
7
|
{ url: 'https://captive.apple.com/hotspot-detect.html' },
|
|
12
8
|
{ method: 'GET', url: 'https://connectivity-check.ubuntu.com' }
|
|
13
9
|
]
|
|
10
|
+
let sharedCheck
|
|
11
|
+
|
|
12
|
+
// Treat the browser's offline flag as a fast failure; confirm online status with a probe.
|
|
13
|
+
export async function isOnline ({ signal } = {}) {
|
|
14
|
+
if (signal?.aborted) throw signal.reason
|
|
15
|
+
if (globalThis.navigator?.onLine === false) return false
|
|
16
|
+
if (signal) return hasInternetConnectivity(signal)
|
|
17
|
+
sharedCheck ??= hasInternetConnectivity().finally(() => { sharedCheck = null })
|
|
18
|
+
return sharedCheck
|
|
19
|
+
}
|
|
14
20
|
|
|
15
|
-
async function hasInternetConnectivity () {
|
|
16
|
-
const
|
|
17
|
-
|
|
21
|
+
async function hasInternetConnectivity (signal) {
|
|
22
|
+
for (const candidate of shuffle(CONNECTIVITY_PROBE_URLS)) {
|
|
23
|
+
if (signal?.aborted) throw signal.reason
|
|
18
24
|
try {
|
|
19
|
-
await ping(candidate.url, { method: candidate.method })
|
|
25
|
+
await ping(candidate.url, { method: candidate.method, signal })
|
|
20
26
|
return true
|
|
21
|
-
} catch
|
|
22
|
-
|
|
27
|
+
} catch {
|
|
28
|
+
if (signal?.aborted) throw signal.reason
|
|
23
29
|
}
|
|
24
30
|
}
|
|
25
31
|
return false
|
|
@@ -34,35 +40,143 @@ function shuffle (list) {
|
|
|
34
40
|
return copy
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (timerId != null) clearTimeout(timerId)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
const timeoutPromise = new Promise((_resolve, reject) => {
|
|
54
|
-
timerId = setTimeout(() => {
|
|
55
|
-
if (abortController) abortController.abort()
|
|
43
|
+
// Bound each probe and release its timer and abort listener on every exit path.
|
|
44
|
+
async function ping (url, { method = 'HEAD', timeout = 5000, signal } = {}) {
|
|
45
|
+
const controller = new AbortController()
|
|
46
|
+
let timer
|
|
47
|
+
let onAbort
|
|
48
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
49
|
+
onAbort = () => {
|
|
50
|
+
controller.abort(signal.reason)
|
|
51
|
+
reject(signal.reason)
|
|
52
|
+
}
|
|
53
|
+
signal?.addEventListener('abort', onAbort, { once: true })
|
|
54
|
+
timer = setTimeout(() => {
|
|
55
|
+
controller.abort()
|
|
56
56
|
reject(new Error('PING_TIMEOUT'))
|
|
57
57
|
}, timeout)
|
|
58
|
+
if (signal?.aborted) onAbort()
|
|
58
59
|
})
|
|
60
|
+
try {
|
|
61
|
+
await Promise.race([
|
|
62
|
+
fetch(url, { method, mode: 'no-cors', cache: 'no-store', redirect: 'follow', signal: controller.signal }),
|
|
63
|
+
stopped
|
|
64
|
+
])
|
|
65
|
+
} finally {
|
|
66
|
+
clearTimeout(timer)
|
|
67
|
+
signal?.removeEventListener('abort', onAbort)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
59
70
|
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
// A monitor owns one probe loop, regardless of how many consumers subscribe.
|
|
72
|
+
export function createConnectivityMonitor ({
|
|
73
|
+
check = isOnline,
|
|
74
|
+
eventTarget = globalThis.window,
|
|
75
|
+
document = globalThis.document,
|
|
76
|
+
_setTimeout = globalThis.setTimeout,
|
|
77
|
+
_clearTimeout = globalThis.clearTimeout,
|
|
78
|
+
_random = Math.random,
|
|
79
|
+
reportError = error => console.error('Online listener failed', error)
|
|
80
|
+
} = {}) {
|
|
81
|
+
if (typeof check !== 'function') throw new ValidationError('INVALID_CONNECTIVITY_CHECK')
|
|
82
|
+
const listeners = new Set()
|
|
83
|
+
const setTimer = (...args) => Reflect.apply(_setTimeout, globalThis, args)
|
|
84
|
+
const clearTimer = (...args) => Reflect.apply(_clearTimeout, globalThis, args)
|
|
85
|
+
let session
|
|
86
|
+
|
|
87
|
+
function deliver (entry, current) {
|
|
88
|
+
if (entry.delivered || !listeners.has(entry) || session !== current) return
|
|
89
|
+
entry.delivered = true
|
|
90
|
+
try { Promise.resolve(entry.handler()).catch(reportError) } catch (error) { reportError(error) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function schedule (current) {
|
|
94
|
+
if (session !== current || !listeners.size) return
|
|
95
|
+
const delay = current.online ? 60000 : RETRY_DELAYS[Math.min(current.retry++, RETRY_DELAYS.length - 1)]
|
|
96
|
+
current.timer = setTimer(() => {
|
|
97
|
+
current.timer = null
|
|
98
|
+
return probe(current)
|
|
99
|
+
}, Math.round(delay * (0.8 + _random() * 0.4)))
|
|
100
|
+
current.timer?.unref?.()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function probe (current) {
|
|
104
|
+
if (session !== current || current.pending) return
|
|
105
|
+
if (current.timer != null) clearTimer(current.timer)
|
|
106
|
+
current.timer = null
|
|
107
|
+
current.pending = true
|
|
108
|
+
try {
|
|
109
|
+
const online = await check({ signal: current.controller.signal })
|
|
110
|
+
if (session !== current) return
|
|
111
|
+
current.online = online === true && globalThis.navigator?.onLine !== false
|
|
112
|
+
if (current.online) {
|
|
113
|
+
current.retry = 0
|
|
114
|
+
for (const entry of [...listeners]) deliver(entry, current)
|
|
115
|
+
} else {
|
|
116
|
+
for (const entry of listeners) entry.delivered = false
|
|
117
|
+
}
|
|
118
|
+
} catch {
|
|
119
|
+
if (session !== current) return
|
|
120
|
+
current.online = false
|
|
121
|
+
for (const entry of listeners) entry.delivered = false
|
|
122
|
+
} finally {
|
|
123
|
+
current.pending = false
|
|
124
|
+
schedule(current)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function start () {
|
|
129
|
+
const current = { online: false, retry: 0, timer: null, pending: false, controller: new AbortController() }
|
|
130
|
+
session = current
|
|
131
|
+
current.wake = () => { probe(current) }
|
|
132
|
+
current.connectionChanged = () => {
|
|
133
|
+
current.online = false
|
|
134
|
+
current.retry = 0
|
|
135
|
+
for (const entry of listeners) entry.delivered = false
|
|
136
|
+
current.wake()
|
|
137
|
+
}
|
|
138
|
+
current.visible = () => { if (document?.visibilityState !== 'hidden') current.wake() }
|
|
139
|
+
eventTarget?.addEventListener('online', current.connectionChanged)
|
|
140
|
+
eventTarget?.addEventListener('offline', current.connectionChanged)
|
|
141
|
+
eventTarget?.addEventListener('focus', current.wake)
|
|
142
|
+
document?.addEventListener('visibilitychange', current.visible)
|
|
143
|
+
queueMicrotask(current.wake)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function stop () {
|
|
147
|
+
const current = session
|
|
148
|
+
session = null
|
|
149
|
+
if (current.timer != null) clearTimer(current.timer)
|
|
150
|
+
current.controller.abort()
|
|
151
|
+
eventTarget?.removeEventListener('online', current.connectionChanged)
|
|
152
|
+
eventTarget?.removeEventListener('offline', current.connectionChanged)
|
|
153
|
+
eventTarget?.removeEventListener('focus', current.wake)
|
|
154
|
+
document?.removeEventListener('visibilitychange', current.visible)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Notify once on confirmed initial connectivity, then after each detected reconnection.
|
|
158
|
+
function onOnline (handler) {
|
|
159
|
+
if (typeof handler !== 'function') throw new ValidationError('INVALID_ONLINE_HANDLER')
|
|
160
|
+
const entry = { handler, delivered: false }
|
|
161
|
+
listeners.add(entry)
|
|
162
|
+
if (!session) start()
|
|
163
|
+
else {
|
|
164
|
+
const current = session
|
|
165
|
+
queueMicrotask(() => { probe(current) })
|
|
166
|
+
}
|
|
167
|
+
return () => {
|
|
168
|
+
if (!listeners.delete(entry)) return
|
|
169
|
+
if (!listeners.size) stop()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { onOnline }
|
|
62
174
|
}
|
|
63
175
|
|
|
176
|
+
let defaultMonitor
|
|
177
|
+
|
|
178
|
+
// Share probes, capped backoff and wake-up listeners across callers in this realm.
|
|
64
179
|
export function onOnline (handler) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
return () => window.removeEventListener('online', listener)
|
|
180
|
+
defaultMonitor ??= createConnectivityMonitor()
|
|
181
|
+
return defaultMonitor.onOnline(handler)
|
|
68
182
|
}
|