libp2r2p 0.10.22 → 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/README.md +18 -0
- package/idb-queue/index.js +29 -0
- package/network/README.md +21 -9
- package/network/index.js +86 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -820,6 +820,24 @@ The NIP-94 extension also carries optional `download` intent; see
|
|
|
820
820
|
|
|
821
821
|
## Reliable IndexedDB queue reservations
|
|
822
822
|
|
|
823
|
+
`queue.putBy(indexName, item, { existingOnly = false })` atomically inserts or
|
|
824
|
+
replaces an item through a declared unique, non-multiEntry index. Its key is
|
|
825
|
+
extracted from the item's index keyPath, including nested paths and compound
|
|
826
|
+
keys. Existing items retain their position; new keys append to the tail. It
|
|
827
|
+
returns `true` after commit, or `false` without inserting when `existingOnly`
|
|
828
|
+
is true and the key is absent. This allows late progress checkpoints to avoid
|
|
829
|
+
recreating a cancelled record. Values are cloned before asynchronous work.
|
|
830
|
+
|
|
831
|
+
Unknown, non-unique or multiEntry indexes throw `ValidationError` with
|
|
832
|
+
`QUEUE_PUT_INDEX_INVALID`; missing or invalid IndexedDB keys use
|
|
833
|
+
`QUEUE_PUT_KEY_INVALID`. Cloning, storage and other unique-index failures retain
|
|
834
|
+
their operational errors. Lookup, replacement and byte accounting share one
|
|
835
|
+
transaction and roll back together. Capacity policy matches `setAt` for a
|
|
836
|
+
replacement and `push` for an insertion. Replacing a reserved item invalidates
|
|
837
|
+
the old token: its `ack`, `nack` and `renew` return false. Successful writes wake
|
|
838
|
+
local waiting consumers. Like the other indexed methods, `putBy` is exclusive
|
|
839
|
+
to `idb-queue`; the Web Storage queue's common API is unchanged.
|
|
840
|
+
|
|
823
841
|
`createQueue({ prefix, evictionPolicy: 'reject', maxBytes })` opts out of
|
|
824
842
|
capacity eviction. Oversized items reject with `QUEUE_ITEM_TOO_LARGE`, full
|
|
825
843
|
queues with `QUEUE_CAPACITY_EXCEEDED`; browser quota errors propagate without
|
package/idb-queue/index.js
CHANGED
|
@@ -725,6 +725,34 @@ export async function createQueue ({
|
|
|
725
725
|
})
|
|
726
726
|
}
|
|
727
727
|
|
|
728
|
+
async function putBy (indexName, item, { existingOnly = false } = {}) {
|
|
729
|
+
const definition = indexDefinitions.find(index => index.name === indexName)
|
|
730
|
+
if (!definition?.unique || definition.multiEntry) throw new ValidationError('QUEUE_PUT_INDEX_INVALID')
|
|
731
|
+
// Snapshot before awaiting IndexedDB so the lookup key and stored value
|
|
732
|
+
// cannot diverge if the caller changes its object during the transaction.
|
|
733
|
+
const value = structuredClone(item)
|
|
734
|
+
const readKey = path => path.split('.').reduce((part, key) => part?.[key], value)
|
|
735
|
+
const key = Array.isArray(definition.keyPath) ? definition.keyPath.map(readKey) : readKey(definition.keyPath)
|
|
736
|
+
try { indexedDB.cmp(key, key) } catch { throw new ValidationError('QUEUE_PUT_KEY_INVALID') }
|
|
737
|
+
const requiredBytes = itemForStorage(0, value).byteSize
|
|
738
|
+
const written = await mutate(async (tx, state) => {
|
|
739
|
+
const { result: previous } = await run('get', [key], ITEMS_STORE, indexName, { tx })
|
|
740
|
+
if (previous) {
|
|
741
|
+
const index = previous.position - state.head
|
|
742
|
+
await putItem(tx, state, previous.position, value, {
|
|
743
|
+
direction: evictionDirectionFor('setAt', { index, length: state.tail - state.head }),
|
|
744
|
+
protectedPositions: new Set([previous.position])
|
|
745
|
+
})
|
|
746
|
+
} else {
|
|
747
|
+
if (existingOnly) return false
|
|
748
|
+
await pushInTransaction(tx, state, value)
|
|
749
|
+
}
|
|
750
|
+
return true
|
|
751
|
+
}, { requiredBytes })
|
|
752
|
+
if (written) wake()
|
|
753
|
+
return written
|
|
754
|
+
}
|
|
755
|
+
|
|
728
756
|
async function getBy (indexName, query) {
|
|
729
757
|
// Explicit index operations avoid scanning queue values in JavaScript.
|
|
730
758
|
return snapshot(async tx => {
|
|
@@ -899,6 +927,7 @@ export async function createQueue ({
|
|
|
899
927
|
removeWhere,
|
|
900
928
|
some,
|
|
901
929
|
clear,
|
|
930
|
+
putBy,
|
|
902
931
|
getBy,
|
|
903
932
|
someBy,
|
|
904
933
|
removeBy,
|
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
|
}
|