@flighthq/connectivity 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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 +3 -3
- package/dist/connectivity.d.ts +10 -17
- package/dist/connectivity.d.ts.map +1 -1
- package/dist/connectivity.js +68 -224
- package/dist/connectivity.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/connectivity.test.ts +238 -371
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flighthq/connectivity
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Explicit connectivity status, change signals, and reachability operations
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/connectivity`. The `
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/connectivity@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/connectivity@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/connectivity)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/connectivity.d.ts
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare function attachConnectivity(
|
|
1
|
+
import type { Connectivity, ConnectivityReachability, ConnectivityReachabilityOptions, ConnectivityStatus, HasConnectivityChange, HasConnectivityReachability, HasConnectivityStatus } from '@flighthq/types/contract';
|
|
2
|
+
export declare function attachConnectivity(host: HasConnectivityStatus & HasConnectivityChange, connectivity: Connectivity): boolean;
|
|
3
3
|
export declare function createConnectivity(): Connectivity;
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function getConnectivityBackend(): ConnectivityBackend;
|
|
11
|
-
export declare function getConnectivityStatus(out: ConnectivityStatus): ConnectivityStatus;
|
|
4
|
+
export declare function destroyConnectivity(host: HasConnectivityChange): void;
|
|
5
|
+
export declare function detachConnectivity(connectivity: Connectivity): void;
|
|
6
|
+
export declare function detectConnectivityReachability(host: HasConnectivityReachability, options: Readonly<ConnectivityReachabilityOptions>, out: ConnectivityReachability): Promise<ConnectivityReachability>;
|
|
7
|
+
export declare function disposeConnectivity(connectivity: Connectivity): void;
|
|
8
|
+
export declare function getConnectivityOnline(host: HasConnectivityStatus): boolean | null;
|
|
9
|
+
export declare function getConnectivityStatus(host: HasConnectivityStatus, out: ConnectivityStatus): ConnectivityStatus;
|
|
12
10
|
export declare function hasConnectivityStatusChanged(a: Readonly<ConnectivityStatus>, b: Readonly<ConnectivityStatus>): boolean;
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function isConnectivityOnline(): boolean;
|
|
16
|
-
export declare function isConnectivitySaveDataEnabled(): boolean;
|
|
17
|
-
export declare function observeConnectivityHostResult(operation: string, succeeded: boolean): void;
|
|
18
|
-
export declare function resetConnectivityBackendForTest(): void;
|
|
19
|
-
export declare function setConnectivityBackend(backend: ConnectivityBackend | null): void;
|
|
11
|
+
export declare function isConnectivityMetered(host: HasConnectivityStatus): boolean;
|
|
12
|
+
export declare function isConnectivitySaveDataEnabled(host: HasConnectivityStatus): boolean;
|
|
20
13
|
//# sourceMappingURL=connectivity.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EACZ,wBAAwB,EACxB,+BAA+B,EAC/B,kBAAkB,EAClB,qBAAqB,EACrB,2BAA2B,EAC3B,qBAAqB,EACtB,MAAM,0BAA0B,CAAC;AAMlC,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,qBAAqB,GAAG,qBAAqB,EACnD,YAAY,EAAE,YAAY,GACzB,OAAO,CA2BT;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAQjD;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,qBAAqB,GAAG,IAAI,CAErE;AAED,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAKnE;AAED,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,2BAA2B,EACjC,OAAO,EAAE,QAAQ,CAAC,+BAA+B,CAAC,EAClD,GAAG,EAAE,wBAAwB,GAC5B,OAAO,CAAC,wBAAwB,CAAC,CAEnC;AAID,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI,CAOpE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,GAAG,IAAI,CAEjF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,EAAE,GAAG,EAAE,kBAAkB,GAAG,kBAAkB,CAE9G;AAED,wBAAgB,4BAA4B,CAC1C,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAC/B,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAC9B,OAAO,CAWT;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAE1E;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,qBAAqB,GAAG,OAAO,CAElF"}
|
package/dist/connectivity.js
CHANGED
|
@@ -1,155 +1,79 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { clearSignal, createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
+
// Starts raw host-change delivery and turns it into the five core-owned diff signals. Status and
|
|
4
|
+
// change are separate Host witnesses: a snapshot command cannot stand in for an event subscription.
|
|
5
|
+
// Re-attaching always consumes the prior provider's exact unsubscribe before touching the new one.
|
|
6
|
+
// Returns false when the change provider cannot establish a real subscription.
|
|
7
|
+
export function attachConnectivity(host, connectivity) {
|
|
8
|
+
detachConnectivity(connectivity);
|
|
9
|
+
const statusBackend = host.connectivity.status;
|
|
10
|
+
const initial = statusBackend.getStatus(connectivityStatusOut());
|
|
6
11
|
let wasOnline = initial.online;
|
|
7
12
|
let wasType = initial.type;
|
|
8
13
|
let wasMetered = initial.metered;
|
|
9
|
-
const unsubscribe =
|
|
10
|
-
const status =
|
|
11
|
-
emitSignal(
|
|
14
|
+
const unsubscribe = host.connectivity.change.subscribe(() => {
|
|
15
|
+
const status = statusBackend.getStatus(connectivityStatusOut());
|
|
16
|
+
emitSignal(connectivity.onChange, status);
|
|
12
17
|
if (status.online !== wasOnline) {
|
|
13
18
|
wasOnline = status.online;
|
|
14
|
-
|
|
19
|
+
if (status.online === true)
|
|
20
|
+
emitSignal(connectivity.onOnline);
|
|
21
|
+
else if (status.online === false)
|
|
22
|
+
emitSignal(connectivity.onOffline);
|
|
15
23
|
}
|
|
16
24
|
if (status.type !== wasType) {
|
|
17
25
|
wasType = status.type;
|
|
18
|
-
emitSignal(
|
|
26
|
+
emitSignal(connectivity.onConnectionTypeChange, status.type);
|
|
19
27
|
}
|
|
20
28
|
if (status.metered !== wasMetered) {
|
|
21
29
|
wasMetered = status.metered;
|
|
22
|
-
emitSignal(
|
|
30
|
+
emitSignal(connectivity.onMeteredChange, status.metered);
|
|
23
31
|
}
|
|
24
32
|
});
|
|
25
|
-
|
|
33
|
+
if (unsubscribe === null)
|
|
34
|
+
return false;
|
|
35
|
+
_subscriptions.set(connectivity, unsubscribe);
|
|
36
|
+
return true;
|
|
26
37
|
}
|
|
27
38
|
export function createConnectivity() {
|
|
28
|
-
return {
|
|
39
|
+
return createEntity({
|
|
29
40
|
onChange: createSignal(),
|
|
30
41
|
onConnectionTypeChange: createSignal(),
|
|
31
42
|
onMeteredChange: createSignal(),
|
|
32
43
|
onOffline: createSignal(),
|
|
33
44
|
onOnline: createSignal(),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
export function createConnectivityStatus() {
|
|
37
|
-
return {
|
|
38
|
-
downlink: -1,
|
|
39
|
-
downlinkMax: -1,
|
|
40
|
-
effectiveType: '',
|
|
41
|
-
metered: false,
|
|
42
|
-
online: false,
|
|
43
|
-
rtt: -1,
|
|
44
|
-
saveData: false,
|
|
45
|
-
type: 'unknown',
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
// Builds the default web backend over navigator.onLine, the Network Information API, and the window
|
|
49
|
-
// online/offline events. Degrades to online=true / type 'unknown' where the APIs are absent.
|
|
50
|
-
export function createWebConnectivityBackend() {
|
|
51
|
-
return {
|
|
52
|
-
getStatus(out) {
|
|
53
|
-
const nav = typeof navigator !== 'undefined' ? navigator : null;
|
|
54
|
-
out.online = nav?.onLine ?? true;
|
|
55
|
-
const conn = getWebConnection();
|
|
56
|
-
out.type = mapWebConnectionType(conn?.type);
|
|
57
|
-
out.downlink = typeof conn?.downlink === 'number' ? conn.downlink : -1;
|
|
58
|
-
out.downlinkMax = typeof conn?.downlinkMax === 'number' ? conn.downlinkMax : -1;
|
|
59
|
-
out.effectiveType = typeof conn?.effectiveType === 'string' ? conn.effectiveType : '';
|
|
60
|
-
out.rtt = typeof conn?.rtt === 'number' ? conn.rtt : -1;
|
|
61
|
-
out.saveData = conn?.saveData === true;
|
|
62
|
-
out.metered = out.saveData || out.type === 'cellular';
|
|
63
|
-
return out;
|
|
64
|
-
},
|
|
65
|
-
async detectReachability(options, out) {
|
|
66
|
-
if (typeof fetch === 'undefined') {
|
|
67
|
-
out.reachable = false;
|
|
68
|
-
out.latency = -1;
|
|
69
|
-
return out;
|
|
70
|
-
}
|
|
71
|
-
const timeout = options.timeout ?? 5000;
|
|
72
|
-
const controller = new AbortController();
|
|
73
|
-
const timerId = setTimeout(() => controller.abort(), timeout);
|
|
74
|
-
const combinedSignal = options.signal ? anyAbortSignal(options.signal, controller.signal) : controller.signal;
|
|
75
|
-
const start = Date.now();
|
|
76
|
-
try {
|
|
77
|
-
const response = await fetch(options.url, {
|
|
78
|
-
method: 'HEAD',
|
|
79
|
-
cache: 'no-store',
|
|
80
|
-
signal: combinedSignal,
|
|
81
|
-
});
|
|
82
|
-
clearTimeout(timerId);
|
|
83
|
-
out.reachable = response.ok;
|
|
84
|
-
out.latency = Date.now() - start;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
clearTimeout(timerId);
|
|
88
|
-
out.reachable = false;
|
|
89
|
-
out.latency = -1;
|
|
90
|
-
}
|
|
91
|
-
return out;
|
|
92
|
-
},
|
|
93
|
-
subscribe(listener) {
|
|
94
|
-
if (typeof window === 'undefined')
|
|
95
|
-
return () => { };
|
|
96
|
-
window.addEventListener('online', listener);
|
|
97
|
-
window.addEventListener('offline', listener);
|
|
98
|
-
const conn = getWebConnection();
|
|
99
|
-
conn?.addEventListener?.('change', listener);
|
|
100
|
-
return () => {
|
|
101
|
-
window.removeEventListener('online', listener);
|
|
102
|
-
window.removeEventListener('offline', listener);
|
|
103
|
-
conn?.removeEventListener?.('change', listener);
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
};
|
|
45
|
+
});
|
|
107
46
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
_subscriptions.delete(net);
|
|
113
|
-
}
|
|
47
|
+
// Terminal provider teardown. This is deliberately separate from per-entity detach: a caller that
|
|
48
|
+
// releases one subscription must not destroy a shared host provider.
|
|
49
|
+
export function destroyConnectivity(host) {
|
|
50
|
+
host.connectivity.change.destroy();
|
|
114
51
|
}
|
|
115
|
-
export
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
return
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (_cachedWebBackend === null)
|
|
122
|
-
_cachedWebBackend = createWebConnectivityBackend();
|
|
123
|
-
const webBackend = _cachedWebBackend;
|
|
124
|
-
if (webBackend.detectReachability !== undefined) {
|
|
125
|
-
return webBackend.detectReachability(options, out);
|
|
126
|
-
}
|
|
127
|
-
out.reachable = false;
|
|
128
|
-
out.latency = -1;
|
|
129
|
-
return out;
|
|
52
|
+
export function detachConnectivity(connectivity) {
|
|
53
|
+
const unsubscribe = _subscriptions.get(connectivity);
|
|
54
|
+
if (unsubscribe === undefined)
|
|
55
|
+
return;
|
|
56
|
+
_subscriptions.delete(connectivity);
|
|
57
|
+
unsubscribe();
|
|
130
58
|
}
|
|
131
|
-
export function
|
|
132
|
-
|
|
59
|
+
export function detectConnectivityReachability(host, options, out) {
|
|
60
|
+
return host.connectivity.reachability.detectReachability(options, out);
|
|
133
61
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
62
|
+
// Disposes only the caller-owned event entity. Provider teardown is terminal and explicit through
|
|
63
|
+
// destroyConnectivity because one provider can fan out to multiple Connectivity entities.
|
|
64
|
+
export function disposeConnectivity(connectivity) {
|
|
65
|
+
detachConnectivity(connectivity);
|
|
66
|
+
clearSignal(connectivity.onChange);
|
|
67
|
+
clearSignal(connectivity.onConnectionTypeChange);
|
|
68
|
+
clearSignal(connectivity.onMeteredChange);
|
|
69
|
+
clearSignal(connectivity.onOffline);
|
|
70
|
+
clearSignal(connectivity.onOnline);
|
|
147
71
|
}
|
|
148
|
-
export function
|
|
149
|
-
return
|
|
72
|
+
export function getConnectivityOnline(host) {
|
|
73
|
+
return host.connectivity.status.getStatus(connectivityStatusOut()).online;
|
|
150
74
|
}
|
|
151
|
-
export function getConnectivityStatus(out) {
|
|
152
|
-
return
|
|
75
|
+
export function getConnectivityStatus(host, out) {
|
|
76
|
+
return host.connectivity.status.getStatus(out);
|
|
153
77
|
}
|
|
154
78
|
export function hasConnectivityStatusChanged(a, b) {
|
|
155
79
|
return (a.online !== b.online ||
|
|
@@ -161,107 +85,27 @@ export function hasConnectivityStatusChanged(a, b) {
|
|
|
161
85
|
a.saveData !== b.saveData ||
|
|
162
86
|
a.metered !== b.metered);
|
|
163
87
|
}
|
|
164
|
-
export function
|
|
165
|
-
|
|
166
|
-
if (_host !== backend)
|
|
167
|
-
_hostConflict = true;
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
_host = backend;
|
|
171
|
-
}
|
|
172
|
-
export function isConnectivityMetered() {
|
|
173
|
-
return getConnectivityBackend().getStatus(_scratch).metered;
|
|
174
|
-
}
|
|
175
|
-
export function isConnectivityOnline() {
|
|
176
|
-
return getConnectivityBackend().getStatus(_scratch).online;
|
|
88
|
+
export function isConnectivityMetered(host) {
|
|
89
|
+
return host.connectivity.status.getStatus(connectivityStatusOut()).metered;
|
|
177
90
|
}
|
|
178
|
-
export function isConnectivitySaveDataEnabled() {
|
|
179
|
-
return
|
|
91
|
+
export function isConnectivitySaveDataEnabled(host) {
|
|
92
|
+
return host.connectivity.status.getStatus(connectivityStatusOut()).saveData;
|
|
180
93
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
94
|
+
// ConnectivityStatus is a backend-produced query/out snapshot, not a user-created identity. Keep its
|
|
95
|
+
// sentinel allocation package-private so the public create* vocabulary remains Entity-only.
|
|
96
|
+
function connectivityStatusOut() {
|
|
97
|
+
return {
|
|
98
|
+
downlink: -1,
|
|
99
|
+
downlinkMax: -1,
|
|
100
|
+
effectiveType: '',
|
|
101
|
+
metered: false,
|
|
102
|
+
online: null,
|
|
103
|
+
rtt: -1,
|
|
104
|
+
saveData: false,
|
|
105
|
+
type: 'unknown',
|
|
185
106
|
};
|
|
186
107
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
_host = null;
|
|
190
|
-
_hostConflict = false;
|
|
191
|
-
_hostObservation = null;
|
|
192
|
-
}
|
|
193
|
-
export function setConnectivityBackend(backend) {
|
|
194
|
-
_custom = backend;
|
|
195
|
-
}
|
|
196
|
-
let _custom = null;
|
|
197
|
-
let _host = null;
|
|
198
|
-
let _hostConflict = false;
|
|
199
|
-
let _hostObservation = null;
|
|
200
|
-
const _scratch = createConnectivityStatus();
|
|
201
|
-
const _sentinel = {
|
|
202
|
-
getStatus(out) {
|
|
203
|
-
out.online = false;
|
|
204
|
-
out.type = 'unknown';
|
|
205
|
-
out.downlink = -1;
|
|
206
|
-
out.downlinkMax = -1;
|
|
207
|
-
out.effectiveType = '';
|
|
208
|
-
out.rtt = -1;
|
|
209
|
-
out.saveData = false;
|
|
210
|
-
out.metered = false;
|
|
211
|
-
return out;
|
|
212
|
-
},
|
|
213
|
-
async detectReachability(_options, out) {
|
|
214
|
-
out.reachable = false;
|
|
215
|
-
out.latency = -1;
|
|
216
|
-
return out;
|
|
217
|
-
},
|
|
218
|
-
subscribe() {
|
|
219
|
-
return () => { };
|
|
220
|
-
},
|
|
221
|
-
};
|
|
108
|
+
// Each entity retains the exact release returned by the provider it attached to. Deleting the entry
|
|
109
|
+
// before invoking the release also makes a re-entrant detach idempotent.
|
|
222
110
|
const _subscriptions = new WeakMap();
|
|
223
|
-
// Combines two AbortSignals: aborts when either fires.
|
|
224
|
-
function anyAbortSignal(a, b) {
|
|
225
|
-
if (typeof AbortSignal !== 'undefined' && 'any' in AbortSignal) {
|
|
226
|
-
return AbortSignal.any([a, b]);
|
|
227
|
-
}
|
|
228
|
-
const controller = new AbortController();
|
|
229
|
-
const onAbort = () => {
|
|
230
|
-
controller.abort();
|
|
231
|
-
a.removeEventListener('abort', onAbort);
|
|
232
|
-
b.removeEventListener('abort', onAbort);
|
|
233
|
-
};
|
|
234
|
-
a.addEventListener('abort', onAbort, { once: true });
|
|
235
|
-
b.addEventListener('abort', onAbort, { once: true });
|
|
236
|
-
return controller.signal;
|
|
237
|
-
}
|
|
238
|
-
function getWebConnection() {
|
|
239
|
-
if (typeof navigator === 'undefined')
|
|
240
|
-
return null;
|
|
241
|
-
const nav = navigator;
|
|
242
|
-
return nav.connection ?? null;
|
|
243
|
-
}
|
|
244
|
-
function mapWebConnectionType(type) {
|
|
245
|
-
switch (type) {
|
|
246
|
-
case 'bluetooth':
|
|
247
|
-
return 'bluetooth';
|
|
248
|
-
case 'cellular':
|
|
249
|
-
return 'cellular';
|
|
250
|
-
case 'ethernet':
|
|
251
|
-
return 'ethernet';
|
|
252
|
-
case 'none':
|
|
253
|
-
return 'none';
|
|
254
|
-
case 'other':
|
|
255
|
-
return 'other';
|
|
256
|
-
case 'vpn':
|
|
257
|
-
return 'vpn';
|
|
258
|
-
case 'wifi':
|
|
259
|
-
return 'wifi';
|
|
260
|
-
case 'wimax':
|
|
261
|
-
return 'wimax';
|
|
262
|
-
default:
|
|
263
|
-
return 'unknown';
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
let _cachedWebBackend = null;
|
|
267
111
|
//# sourceMappingURL=connectivity.js.map
|
package/dist/connectivity.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connectivity.js","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"connectivity.js","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { attachConnectivity, createConnectivity, detachConnectivity, detectConnectivityReachability, disposeConnectivity, destroyConnectivity, getConnectivityOnline, getConnectivityStatus, hasConnectivityStatusChanged, isConnectivityMetered, isConnectivitySaveDataEnabled, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,8BAA8B,EAC9B,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { attachConnectivity, createConnectivity, detachConnectivity, detectConnectivityReachability, disposeConnectivity, destroyConnectivity, getConnectivityOnline, getConnectivityStatus, hasConnectivityStatusChanged, isConnectivityMetered, isConnectivitySaveDataEnabled, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,8BAA8B,EAC9B,mBAAmB,EACnB,mBAAmB,EACnB,qBAAqB,EACrB,qBAAqB,EACrB,4BAA4B,EAC5B,qBAAqB,EACrB,6BAA6B,GAC9B,MAAM,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/connectivity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,12 +38,13 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/entity": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
|
|
43
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"typescript": "^5.3.0"
|
|
46
47
|
},
|
|
47
|
-
"description": "
|
|
48
|
+
"description": "Explicit connectivity status, change signals, and reachability operations",
|
|
48
49
|
"sideEffects": false
|
|
49
50
|
}
|
package/src/connectivity.test.ts
CHANGED
|
@@ -1,453 +1,320 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import { connectSignal, hasSignalSlots } from '@flighthq/signals/contract';
|
|
3
|
+
import type {
|
|
4
|
+
ConnectivityChangeBackend,
|
|
5
|
+
ConnectivityReachabilityBackend,
|
|
6
|
+
ConnectivityStatus,
|
|
7
|
+
ConnectivityStatusBackend,
|
|
8
|
+
} from '@flighthq/types/contract';
|
|
9
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
3
10
|
|
|
4
11
|
import {
|
|
5
12
|
attachConnectivity,
|
|
6
13
|
createConnectivity,
|
|
7
|
-
createConnectivityStatus,
|
|
8
|
-
createWebConnectivityBackend,
|
|
9
14
|
detachConnectivity,
|
|
10
15
|
detectConnectivityReachability,
|
|
11
16
|
disposeConnectivity,
|
|
12
|
-
|
|
17
|
+
destroyConnectivity,
|
|
18
|
+
getConnectivityOnline,
|
|
13
19
|
getConnectivityStatus,
|
|
14
20
|
hasConnectivityStatusChanged,
|
|
15
21
|
isConnectivityMetered,
|
|
16
|
-
isConnectivityOnline,
|
|
17
22
|
isConnectivitySaveDataEnabled,
|
|
18
|
-
setConnectivityBackend,
|
|
19
|
-
explainConnectivityBackend,
|
|
20
|
-
installConnectivityHostBackend,
|
|
21
|
-
observeConnectivityHostResult,
|
|
22
|
-
resetConnectivityBackendForTest,
|
|
23
23
|
} from './connectivity';
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
interface FakeConnectivityProvider extends ConnectivityStatusBackend, ConnectivityChangeBackend {
|
|
26
|
+
readonly activeSubscriptions: () => number;
|
|
27
|
+
readonly destroyCalls: () => number;
|
|
28
|
+
fire(): void;
|
|
29
|
+
readonly status: ConnectivityStatus;
|
|
30
|
+
readonly unsubscribeCalls: () => number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fakeProvider(
|
|
34
|
+
overrides: Partial<ConnectivityStatus> = {},
|
|
35
|
+
subscriptionAvailable = true,
|
|
36
|
+
): FakeConnectivityProvider {
|
|
37
|
+
const listeners = new Set<() => void>();
|
|
38
|
+
const current = status({
|
|
30
39
|
downlink: 10,
|
|
31
40
|
downlinkMax: 100,
|
|
32
41
|
effectiveType: '4g',
|
|
33
|
-
metered: false,
|
|
34
42
|
online: true,
|
|
35
43
|
rtt: 50,
|
|
36
|
-
saveData: false,
|
|
37
44
|
type: 'wifi',
|
|
38
45
|
...overrides,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
});
|
|
47
|
+
let destroys = 0;
|
|
48
|
+
let unsubscribes = 0;
|
|
49
|
+
let destroyed = false;
|
|
50
|
+
return Object.assign(
|
|
51
|
+
createEntity({
|
|
52
|
+
destroy() {
|
|
53
|
+
if (destroyed) return;
|
|
54
|
+
destroyed = true;
|
|
55
|
+
destroys++;
|
|
56
|
+
listeners.clear();
|
|
57
|
+
},
|
|
58
|
+
getStatus(out: ConnectivityStatus) {
|
|
59
|
+
Object.assign(out, current);
|
|
60
|
+
return out;
|
|
61
|
+
},
|
|
62
|
+
subscribe(listener: () => void) {
|
|
63
|
+
if (!subscriptionAvailable || destroyed) return null;
|
|
64
|
+
listeners.add(listener);
|
|
65
|
+
let active = true;
|
|
66
|
+
return () => {
|
|
67
|
+
if (!active) return;
|
|
68
|
+
active = false;
|
|
69
|
+
unsubscribes++;
|
|
70
|
+
listeners.delete(listener);
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
{
|
|
75
|
+
activeSubscriptions: () => listeners.size,
|
|
76
|
+
destroyCalls: () => destroys,
|
|
77
|
+
fire: () => {
|
|
78
|
+
for (const listener of [...listeners]) listener();
|
|
79
|
+
},
|
|
80
|
+
status: current,
|
|
81
|
+
unsubscribeCalls: () => unsubscribes,
|
|
61
82
|
},
|
|
62
|
-
|
|
83
|
+
);
|
|
63
84
|
}
|
|
64
85
|
|
|
65
|
-
|
|
86
|
+
function hostFor(provider: FakeConnectivityProvider) {
|
|
87
|
+
return { connectivity: { change: provider, status: provider } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function status(overrides: Partial<ConnectivityStatus> = {}): ConnectivityStatus {
|
|
91
|
+
return {
|
|
92
|
+
downlink: -1,
|
|
93
|
+
downlinkMax: -1,
|
|
94
|
+
effectiveType: '',
|
|
95
|
+
metered: false,
|
|
96
|
+
online: null,
|
|
97
|
+
rtt: -1,
|
|
98
|
+
saveData: false,
|
|
99
|
+
type: 'unknown',
|
|
100
|
+
...overrides,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
66
103
|
|
|
67
104
|
describe('attachConnectivity', () => {
|
|
68
|
-
it('
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
105
|
+
it('returns true and emits core diffs from a raw provider change', () => {
|
|
106
|
+
const provider = fakeProvider();
|
|
107
|
+
const connectivity = createConnectivity();
|
|
108
|
+
const changes: ConnectivityStatus[] = [];
|
|
109
|
+
const types: string[] = [];
|
|
110
|
+
const metered: boolean[] = [];
|
|
73
111
|
let offline = 0;
|
|
74
|
-
connectSignal(
|
|
75
|
-
connectSignal(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
(backend as ReturnType<typeof fakeBackend>).fire();
|
|
79
|
-
expect(changes).toBe(1);
|
|
80
|
-
expect(offline).toBe(1);
|
|
81
|
-
});
|
|
112
|
+
connectSignal(connectivity.onChange, (value) => changes.push(value as ConnectivityStatus));
|
|
113
|
+
connectSignal(connectivity.onConnectionTypeChange, (value) => types.push(value));
|
|
114
|
+
connectSignal(connectivity.onMeteredChange, (value) => metered.push(value));
|
|
115
|
+
connectSignal(connectivity.onOffline, () => offline++);
|
|
82
116
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const net = createConnectivity();
|
|
87
|
-
let online = 0;
|
|
88
|
-
connectSignal(net.onOnline, () => online++);
|
|
89
|
-
attachConnectivity(net);
|
|
90
|
-
backend.status.online = true;
|
|
91
|
-
(backend as ReturnType<typeof fakeBackend>).fire();
|
|
92
|
-
expect(online).toBe(1);
|
|
93
|
-
});
|
|
117
|
+
expect(attachConnectivity(hostFor(provider), connectivity)).toBe(true);
|
|
118
|
+
Object.assign(provider.status, { metered: true, online: false, type: 'cellular' as const });
|
|
119
|
+
provider.fire();
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
setConnectivityBackend(backend);
|
|
98
|
-
const net = createConnectivity();
|
|
99
|
-
const types: string[] = [];
|
|
100
|
-
connectSignal(net.onConnectionTypeChange, (t) => types.push(t));
|
|
101
|
-
attachConnectivity(net);
|
|
102
|
-
backend.status.type = 'cellular';
|
|
103
|
-
(backend as ReturnType<typeof fakeBackend>).fire();
|
|
121
|
+
expect(changes).toHaveLength(1);
|
|
122
|
+
expect(changes[0]).toMatchObject({ metered: true, online: false, type: 'cellular' });
|
|
104
123
|
expect(types).toEqual(['cellular']);
|
|
124
|
+
expect(metered).toEqual([true]);
|
|
125
|
+
expect(offline).toBe(1);
|
|
105
126
|
});
|
|
106
127
|
|
|
107
|
-
it('
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
it('treats unknown as unmeasured and emits offline only after a measured transition', () => {
|
|
129
|
+
const provider = fakeProvider({ online: null });
|
|
130
|
+
const connectivity = createConnectivity();
|
|
131
|
+
let offline = 0;
|
|
132
|
+
connectSignal(connectivity.onOffline, () => offline++);
|
|
133
|
+
attachConnectivity(hostFor(provider), connectivity);
|
|
134
|
+
provider.fire();
|
|
135
|
+
expect(offline).toBe(0);
|
|
136
|
+
provider.status.online = false;
|
|
137
|
+
provider.fire();
|
|
138
|
+
expect(offline).toBe(1);
|
|
117
139
|
});
|
|
118
140
|
|
|
119
|
-
it('
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const metered: boolean[] = [];
|
|
124
|
-
connectSignal(net.onMeteredChange, (m) => metered.push(m));
|
|
125
|
-
attachConnectivity(net);
|
|
126
|
-
backend.status.metered = true;
|
|
127
|
-
(backend as ReturnType<typeof fakeBackend>).fire();
|
|
128
|
-
expect(metered).toEqual([true]);
|
|
141
|
+
it('returns false instead of retaining a silent no-op subscription', () => {
|
|
142
|
+
const provider = fakeProvider({}, false);
|
|
143
|
+
expect(attachConnectivity(hostFor(provider), createConnectivity())).toBe(false);
|
|
144
|
+
expect(provider.activeSubscriptions()).toBe(0);
|
|
129
145
|
});
|
|
130
146
|
|
|
131
|
-
it('
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
147
|
+
it('detaches A before attaching B and never delivers from the old origin', () => {
|
|
148
|
+
const a = fakeProvider();
|
|
149
|
+
const b = fakeProvider();
|
|
150
|
+
const connectivity = createConnectivity();
|
|
135
151
|
let changes = 0;
|
|
136
|
-
connectSignal(
|
|
137
|
-
attachConnectivity(
|
|
138
|
-
attachConnectivity(
|
|
139
|
-
(
|
|
152
|
+
connectSignal(connectivity.onChange, () => changes++);
|
|
153
|
+
attachConnectivity(hostFor(a), connectivity);
|
|
154
|
+
attachConnectivity(hostFor(b), connectivity);
|
|
155
|
+
expect(a.unsubscribeCalls()).toBe(1);
|
|
156
|
+
expect(a.activeSubscriptions()).toBe(0);
|
|
157
|
+
expect(b.activeSubscriptions()).toBe(1);
|
|
158
|
+
a.fire();
|
|
159
|
+
expect(changes).toBe(0);
|
|
160
|
+
b.fire();
|
|
140
161
|
expect(changes).toBe(1);
|
|
141
162
|
});
|
|
142
|
-
});
|
|
143
163
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
164
|
+
it('emits distinct status snapshots rather than mutating a retained prior payload', () => {
|
|
165
|
+
const provider = fakeProvider();
|
|
166
|
+
const connectivity = createConnectivity();
|
|
167
|
+
const changes: Readonly<ConnectivityStatus>[] = [];
|
|
168
|
+
connectSignal(connectivity.onChange, (value) => changes.push(value));
|
|
169
|
+
attachConnectivity(hostFor(provider), connectivity);
|
|
170
|
+
provider.fire();
|
|
171
|
+
provider.status.type = 'cellular';
|
|
172
|
+
provider.fire();
|
|
173
|
+
expect(changes[0]).not.toBe(changes[1]);
|
|
174
|
+
expect(changes[0]?.type).toBe('wifi');
|
|
175
|
+
expect(changes[1]?.type).toBe('cellular');
|
|
152
176
|
});
|
|
153
177
|
});
|
|
154
178
|
|
|
155
|
-
describe('
|
|
156
|
-
it('
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
});
|
|
179
|
+
describe('createConnectivity', () => {
|
|
180
|
+
it('returns an Entity carrying all five core-owned signals', () => {
|
|
181
|
+
const connectivity = createConnectivity();
|
|
182
|
+
expect(EntityRuntimeKey in connectivity).toBe(true);
|
|
183
|
+
expect(Object.keys(connectivity).sort()).toEqual([
|
|
184
|
+
'onChange',
|
|
185
|
+
'onConnectionTypeChange',
|
|
186
|
+
'onMeteredChange',
|
|
187
|
+
'onOffline',
|
|
188
|
+
'onOnline',
|
|
189
|
+
]);
|
|
167
190
|
});
|
|
168
191
|
});
|
|
169
192
|
|
|
170
|
-
describe('
|
|
171
|
-
it('
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
expect(
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
it('returns sentinel rtt and downlinkMax when connection is absent', () => {
|
|
180
|
-
const out = createConnectivityStatus();
|
|
181
|
-
createWebConnectivityBackend().getStatus(out);
|
|
182
|
-
// In jsdom, navigator.connection is absent so rtt/downlinkMax default to -1
|
|
183
|
-
expect(out.rtt).toBe(-1);
|
|
184
|
-
expect(out.downlinkMax).toBe(-1);
|
|
193
|
+
describe('destroyConnectivity', () => {
|
|
194
|
+
it('uses the supplied change provider and provider teardown is terminal/idempotent', () => {
|
|
195
|
+
const provider = fakeProvider();
|
|
196
|
+
const host = hostFor(provider);
|
|
197
|
+
destroyConnectivity(host);
|
|
198
|
+
destroyConnectivity(host);
|
|
199
|
+
expect(provider.destroyCalls()).toBe(1);
|
|
200
|
+
expect(provider.subscribe(() => {})).toBeNull();
|
|
185
201
|
});
|
|
186
202
|
});
|
|
187
203
|
|
|
188
204
|
describe('detachConnectivity', () => {
|
|
189
|
-
it('
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
detachConnectivity(net);
|
|
197
|
-
(backend as ReturnType<typeof fakeBackend>).fire();
|
|
198
|
-
expect(changes).toBe(0);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('is safe to call when not attached', () => {
|
|
202
|
-
const net = createConnectivity();
|
|
203
|
-
expect(() => detachConnectivity(net)).not.toThrow();
|
|
205
|
+
it('consumes the exact unsubscribe once', () => {
|
|
206
|
+
const provider = fakeProvider();
|
|
207
|
+
const connectivity = createConnectivity();
|
|
208
|
+
attachConnectivity(hostFor(provider), connectivity);
|
|
209
|
+
detachConnectivity(connectivity);
|
|
210
|
+
detachConnectivity(connectivity);
|
|
211
|
+
expect(provider.unsubscribeCalls()).toBe(1);
|
|
204
212
|
});
|
|
205
213
|
});
|
|
206
214
|
|
|
207
215
|
describe('detectConnectivityReachability', () => {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
// the real fetch between tests.
|
|
215
|
-
//
|
|
216
|
-
// The assertion is the sentinel itself — reachable false AND latency -1 — rather than the shape of
|
|
217
|
-
// the value. A `typeof result.reachable === 'boolean'` check passes on a live success too, so it held
|
|
218
|
-
// for a test whose name promises a sentinel while checking nothing that distinguishes one.
|
|
219
|
-
it('returns a sentinel when fetch is unavailable (SSR/jsdom guard)', async () => {
|
|
220
|
-
setConnectivityBackend(fakeBackend());
|
|
221
|
-
vi.stubGlobal('fetch', undefined);
|
|
222
|
-
const out: ConnectivityReachability = { latency: 0, reachable: true };
|
|
223
|
-
const result = await detectConnectivityReachability({ url: 'https://example.com' }, out);
|
|
224
|
-
expect(result).toBe(out);
|
|
225
|
-
expect(result.reachable).toBe(false);
|
|
226
|
-
expect(result.latency).toBe(-1);
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
// The fallback's other sentinel path: fetch exists and the HEAD request rejects. Same contract, and
|
|
230
|
-
// it was unpinned — the absent-fetch branch could have returned the sentinel while this one did not.
|
|
231
|
-
it('returns a sentinel when the HEAD request fails', async () => {
|
|
232
|
-
setConnectivityBackend(fakeBackend());
|
|
233
|
-
vi.stubGlobal('fetch', () => Promise.reject(new Error('network unreachable')));
|
|
234
|
-
const out: ConnectivityReachability = { latency: 0, reachable: true };
|
|
235
|
-
const result = await detectConnectivityReachability({ url: 'https://example.com' }, out);
|
|
236
|
-
expect(result).toBe(out);
|
|
237
|
-
expect(result.reachable).toBe(false);
|
|
238
|
-
expect(result.latency).toBe(-1);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('uses the backend detectReachability when available', async () => {
|
|
242
|
-
const probeBackend: ConnectivityBackend = {
|
|
243
|
-
...fakeBackend(),
|
|
244
|
-
async detectReachability(_opts, out) {
|
|
216
|
+
it('dispatches reachability only to the supplied reachability slot', async () => {
|
|
217
|
+
let calls = 0;
|
|
218
|
+
const reachability = createEntity({
|
|
219
|
+
async detectReachability(_options, out) {
|
|
220
|
+
calls++;
|
|
221
|
+
out.latency = 7;
|
|
245
222
|
out.reachable = true;
|
|
246
|
-
out.latency = 42;
|
|
247
223
|
return out;
|
|
248
224
|
},
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
225
|
+
} satisfies Omit<ConnectivityReachabilityBackend, typeof EntityRuntimeKey>);
|
|
226
|
+
const out = { latency: -1, reachable: false };
|
|
227
|
+
const result = await detectConnectivityReachability(
|
|
228
|
+
{ connectivity: { reachability } },
|
|
229
|
+
{ url: 'https://example.invalid' },
|
|
230
|
+
out,
|
|
231
|
+
);
|
|
232
|
+
expect(result).toBe(out);
|
|
233
|
+
expect(result).toEqual({ latency: 7, reachable: true });
|
|
234
|
+
expect(calls).toBe(1);
|
|
255
235
|
});
|
|
256
236
|
});
|
|
257
237
|
|
|
258
238
|
describe('disposeConnectivity', () => {
|
|
259
|
-
it('
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const explanation = explainConnectivityBackend();
|
|
279
|
-
expect(explanation.layer).toBe('host-not-enabled');
|
|
280
|
-
expect(explanation.conflict).toBe(false);
|
|
281
|
-
expect(explanation.viability).toBe('unobserved');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('reports custom layer when a custom backend is set', () => {
|
|
285
|
-
setConnectivityBackend(fakeBackend());
|
|
286
|
-
expect(explainConnectivityBackend().layer).toBe('custom');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('reports host layer when a host backend is installed', () => {
|
|
290
|
-
installConnectivityHostBackend(fakeBackend());
|
|
291
|
-
expect(explainConnectivityBackend().layer).toBe('host');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('reports conflict when two different host backends are installed', () => {
|
|
295
|
-
installConnectivityHostBackend(fakeBackend());
|
|
296
|
-
installConnectivityHostBackend(fakeBackend());
|
|
297
|
-
expect(explainConnectivityBackend().conflict).toBe(true);
|
|
239
|
+
it('unsubscribes once and clears listeners from every signal', () => {
|
|
240
|
+
const provider = fakeProvider();
|
|
241
|
+
const connectivity = createConnectivity();
|
|
242
|
+
attachConnectivity(hostFor(provider), connectivity);
|
|
243
|
+
connectSignal(connectivity.onChange, () => {});
|
|
244
|
+
connectSignal(connectivity.onConnectionTypeChange, () => {});
|
|
245
|
+
connectSignal(connectivity.onMeteredChange, () => {});
|
|
246
|
+
connectSignal(connectivity.onOffline, () => {});
|
|
247
|
+
connectSignal(connectivity.onOnline, () => {});
|
|
248
|
+
disposeConnectivity(connectivity);
|
|
249
|
+
disposeConnectivity(connectivity);
|
|
250
|
+
expect(provider.unsubscribeCalls()).toBe(1);
|
|
251
|
+
expect([
|
|
252
|
+
hasSignalSlots(connectivity.onChange),
|
|
253
|
+
hasSignalSlots(connectivity.onConnectionTypeChange),
|
|
254
|
+
hasSignalSlots(connectivity.onMeteredChange),
|
|
255
|
+
hasSignalSlots(connectivity.onOffline),
|
|
256
|
+
hasSignalSlots(connectivity.onOnline),
|
|
257
|
+
]).toEqual([false, false, false, false, false]);
|
|
298
258
|
});
|
|
299
259
|
});
|
|
300
260
|
|
|
301
|
-
describe('
|
|
302
|
-
it('
|
|
303
|
-
expect(
|
|
261
|
+
describe('getConnectivityOnline', () => {
|
|
262
|
+
it('preserves unknown and measured online states', () => {
|
|
263
|
+
expect(getConnectivityOnline(hostFor(fakeProvider({ online: null })))).toBeNull();
|
|
264
|
+
expect(getConnectivityOnline(hostFor(fakeProvider({ online: true })))).toBe(true);
|
|
304
265
|
});
|
|
305
266
|
});
|
|
306
267
|
|
|
307
268
|
describe('getConnectivityStatus', () => {
|
|
308
|
-
it('
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
269
|
+
it('reads status and level conveniences only from the supplied host', () => {
|
|
270
|
+
const provider = fakeProvider({ metered: true, online: false, saveData: true });
|
|
271
|
+
const host = hostFor(provider);
|
|
272
|
+
const out = status();
|
|
273
|
+
expect(getConnectivityStatus(host, out)).toBe(out);
|
|
312
274
|
expect(out.type).toBe('wifi');
|
|
313
|
-
expect(out.rtt).toBe(50);
|
|
314
|
-
expect(out.downlinkMax).toBe(100);
|
|
315
275
|
});
|
|
316
276
|
});
|
|
317
277
|
|
|
318
278
|
describe('hasConnectivityStatusChanged', () => {
|
|
319
|
-
it('returns false for equal
|
|
320
|
-
const a =
|
|
321
|
-
|
|
322
|
-
expect(hasConnectivityStatusChanged(a,
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it('
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it('returns true when metered differs', () => {
|
|
350
|
-
const a = { ...createConnectivityStatus(), metered: true };
|
|
351
|
-
const b = createConnectivityStatus();
|
|
352
|
-
expect(hasConnectivityStatusChanged(a, b)).toBe(true);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it('is alias-safe — same object returns false', () => {
|
|
356
|
-
const s = createConnectivityStatus();
|
|
357
|
-
expect(hasConnectivityStatusChanged(s, s)).toBe(false);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe('installConnectivityHostBackend', () => {
|
|
362
|
-
afterEach(() => resetConnectivityBackendForTest());
|
|
363
|
-
|
|
364
|
-
it('installs a host backend that getConnectivityBackend returns', () => {
|
|
365
|
-
const backend = fakeBackend();
|
|
366
|
-
installConnectivityHostBackend(backend);
|
|
367
|
-
expect(getConnectivityBackend()).toBe(backend);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('is first-host-wins: a second different backend sets conflict', () => {
|
|
371
|
-
const first = fakeBackend();
|
|
372
|
-
const second = fakeBackend();
|
|
373
|
-
installConnectivityHostBackend(first);
|
|
374
|
-
installConnectivityHostBackend(second);
|
|
375
|
-
expect(getConnectivityBackend()).toBe(first);
|
|
376
|
-
expect(explainConnectivityBackend().conflict).toBe(true);
|
|
279
|
+
it('returns false for equal or aliased snapshots', () => {
|
|
280
|
+
const a = status();
|
|
281
|
+
expect(hasConnectivityStatusChanged(a, { ...a })).toBe(false);
|
|
282
|
+
expect(hasConnectivityStatusChanged(a, a)).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('detects every status axis', () => {
|
|
286
|
+
const base = status();
|
|
287
|
+
const variants: ConnectivityStatus[] = [
|
|
288
|
+
status({ online: true }),
|
|
289
|
+
status({ type: 'wifi' }),
|
|
290
|
+
status({ downlink: 1 }),
|
|
291
|
+
status({ downlinkMax: 1 }),
|
|
292
|
+
status({ effectiveType: '4g' }),
|
|
293
|
+
status({ rtt: 1 }),
|
|
294
|
+
status({ saveData: true }),
|
|
295
|
+
status({ metered: true }),
|
|
296
|
+
];
|
|
297
|
+
expect(variants.map((variant) => hasConnectivityStatusChanged(base, variant))).toEqual([
|
|
298
|
+
true,
|
|
299
|
+
true,
|
|
300
|
+
true,
|
|
301
|
+
true,
|
|
302
|
+
true,
|
|
303
|
+
true,
|
|
304
|
+
true,
|
|
305
|
+
true,
|
|
306
|
+
]);
|
|
377
307
|
});
|
|
378
308
|
});
|
|
379
309
|
|
|
380
310
|
describe('isConnectivityMetered', () => {
|
|
381
|
-
it('
|
|
382
|
-
|
|
383
|
-
expect(isConnectivityMetered()).toBe(false);
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it('returns true for a metered connection', () => {
|
|
387
|
-
setConnectivityBackend(fakeBackend({ metered: true }));
|
|
388
|
-
expect(isConnectivityMetered()).toBe(true);
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
describe('isConnectivityOnline', () => {
|
|
393
|
-
it('reflects the backend online flag', () => {
|
|
394
|
-
const backend = fakeBackend({ online: false });
|
|
395
|
-
setConnectivityBackend(backend);
|
|
396
|
-
expect(isConnectivityOnline()).toBe(false);
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
it('returns true when the backend is online', () => {
|
|
400
|
-
setConnectivityBackend(fakeBackend({ online: true }));
|
|
401
|
-
expect(isConnectivityOnline()).toBe(true);
|
|
311
|
+
it('reads the metered level from the supplied status witness', () => {
|
|
312
|
+
expect(isConnectivityMetered(hostFor(fakeProvider({ metered: true })))).toBe(true);
|
|
402
313
|
});
|
|
403
314
|
});
|
|
404
315
|
|
|
405
316
|
describe('isConnectivitySaveDataEnabled', () => {
|
|
406
|
-
it('
|
|
407
|
-
|
|
408
|
-
expect(isConnectivitySaveDataEnabled()).toBe(false);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it('returns true when saveData is on', () => {
|
|
412
|
-
setConnectivityBackend(fakeBackend({ saveData: true }));
|
|
413
|
-
expect(isConnectivitySaveDataEnabled()).toBe(true);
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
describe('observeConnectivityHostResult', () => {
|
|
418
|
-
afterEach(() => resetConnectivityBackendForTest());
|
|
419
|
-
|
|
420
|
-
it('records a successful observation', () => {
|
|
421
|
-
installConnectivityHostBackend(fakeBackend());
|
|
422
|
-
observeConnectivityHostResult('getStatus', true);
|
|
423
|
-
const explanation = explainConnectivityBackend();
|
|
424
|
-
expect(explanation.operation).toBe('getStatus');
|
|
425
|
-
expect(explanation.viability).toBe('available');
|
|
426
|
-
});
|
|
427
|
-
|
|
428
|
-
it('records a failed observation', () => {
|
|
429
|
-
installConnectivityHostBackend(fakeBackend());
|
|
430
|
-
observeConnectivityHostResult('getStatus', false);
|
|
431
|
-
expect(explainConnectivityBackend().viability).toBe('runtime-api-unavailable');
|
|
432
|
-
});
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
describe('resetConnectivityBackendForTest', () => {
|
|
436
|
-
it('clears all backend slots', () => {
|
|
437
|
-
setConnectivityBackend(fakeBackend());
|
|
438
|
-
installConnectivityHostBackend(fakeBackend());
|
|
439
|
-
observeConnectivityHostResult('getStatus', true);
|
|
440
|
-
resetConnectivityBackendForTest();
|
|
441
|
-
expect(explainConnectivityBackend().layer).toBe('host-not-enabled');
|
|
442
|
-
expect(explainConnectivityBackend().conflict).toBe(false);
|
|
443
|
-
expect(explainConnectivityBackend().viability).toBe('unobserved');
|
|
444
|
-
});
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
describe('setConnectivityBackend', () => {
|
|
448
|
-
it('clears back to the web fallback when passed null', () => {
|
|
449
|
-
setConnectivityBackend(fakeBackend());
|
|
450
|
-
setConnectivityBackend(null);
|
|
451
|
-
expect(getConnectivityBackend()).not.toBeNull();
|
|
317
|
+
it('reads the save-data level from the supplied status witness', () => {
|
|
318
|
+
expect(isConnectivitySaveDataEnabled(hostFor(fakeProvider({ saveData: true })))).toBe(true);
|
|
452
319
|
});
|
|
453
320
|
});
|