@flighthq/connectivity 0.1.0

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.
@@ -0,0 +1,16 @@
1
+ import type { Connectivity, ConnectivityBackend, ConnectivityReachability, ConnectivityReachabilityOptions, ConnectivityStatus } from '@flighthq/types';
2
+ export declare function attachConnectivity(net: Connectivity): void;
3
+ export declare function createConnectivity(): Connectivity;
4
+ export declare function createConnectivityStatus(): ConnectivityStatus;
5
+ export declare function createWebConnectivityBackend(): ConnectivityBackend;
6
+ export declare function detachConnectivity(net: Connectivity): void;
7
+ export declare function detectConnectivityReachability(options: Readonly<ConnectivityReachabilityOptions>, out: ConnectivityReachability): Promise<ConnectivityReachability>;
8
+ export declare function disposeConnectivity(net: Connectivity): void;
9
+ export declare function getConnectivityBackend(): ConnectivityBackend;
10
+ export declare function getConnectivityStatus(out: ConnectivityStatus): ConnectivityStatus;
11
+ export declare function hasConnectivityStatusChanged(a: Readonly<ConnectivityStatus>, b: Readonly<ConnectivityStatus>): boolean;
12
+ export declare function isConnectivityMetered(): boolean;
13
+ export declare function isConnectivityOnline(): boolean;
14
+ export declare function isConnectivitySaveDataEnabled(): boolean;
15
+ export declare function setConnectivityBackend(backend: ConnectivityBackend | null): void;
16
+ //# sourceMappingURL=connectivity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectivity.d.ts","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EAEnB,wBAAwB,EACxB,+BAA+B,EAC/B,kBAAkB,EACnB,MAAM,iBAAiB,CAAC;AAKzB,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAwB1D;AAGD,wBAAgB,kBAAkB,IAAI,YAAY,CAQjD;AAGD,wBAAgB,wBAAwB,IAAI,kBAAkB,CAW7D;AAID,wBAAgB,4BAA4B,IAAI,mBAAmB,CAuDlE;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAM1D;AAOD,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,QAAQ,CAAC,+BAA+B,CAAC,EAClD,GAAG,EAAE,wBAAwB,GAC5B,OAAO,CAAC,wBAAwB,CAAC,CAcnC;AAID,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAE3D;AAGD,wBAAgB,sBAAsB,IAAI,mBAAmB,CAG5D;AAGD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,kBAAkB,GAAG,kBAAkB,CAEjF;AAGD,wBAAgB,4BAA4B,CAC1C,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EAC/B,CAAC,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAC9B,OAAO,CAWT;AAGD,wBAAgB,qBAAqB,IAAI,OAAO,CAE/C;AAGD,wBAAgB,oBAAoB,IAAI,OAAO,CAE9C;AAGD,wBAAgB,6BAA6B,IAAI,OAAO,CAEvD;AAGD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,GAAG,IAAI,CAEhF"}
@@ -0,0 +1,231 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ // Begins delivering connectivity changes to `net`'s signals by subscribing to the active backend. On
3
+ // each change it reads a fresh status and emits onChange plus edge-triggered signals. Idempotent: a
4
+ // prior subscription is torn down first. Pair with detachConnectivity/disposeConnectivity.
5
+ export function attachConnectivity(net) {
6
+ detachConnectivity(net);
7
+ const backend = getConnectivityBackend();
8
+ const initial = backend.getStatus(_scratch);
9
+ let wasOnline = initial.online;
10
+ let wasType = initial.type;
11
+ let wasMetered = initial.metered;
12
+ const unsubscribe = backend.subscribe(() => {
13
+ const status = backend.getStatus(_scratch);
14
+ emitSignal(net.onChange, status);
15
+ if (status.online !== wasOnline) {
16
+ wasOnline = status.online;
17
+ emitSignal(status.online ? net.onOnline : net.onOffline);
18
+ }
19
+ if (status.type !== wasType) {
20
+ wasType = status.type;
21
+ emitSignal(net.onConnectionTypeChange, status.type);
22
+ }
23
+ if (status.metered !== wasMetered) {
24
+ wasMetered = status.metered;
25
+ emitSignal(net.onMeteredChange, status.metered);
26
+ }
27
+ });
28
+ _subscriptions.set(net, unsubscribe);
29
+ }
30
+ // Allocates a Connectivity event entity with inert signals; call attachConnectivity to start delivery.
31
+ export function createConnectivity() {
32
+ return {
33
+ onChange: createSignal(),
34
+ onConnectionTypeChange: createSignal(),
35
+ onMeteredChange: createSignal(),
36
+ onOffline: createSignal(),
37
+ onOnline: createSignal(),
38
+ };
39
+ }
40
+ // Allocates a zeroed ConnectivityStatus, suitable as the `out` for getConnectivityStatus.
41
+ export function createConnectivityStatus() {
42
+ return {
43
+ downlink: -1,
44
+ downlinkMax: -1,
45
+ effectiveType: '',
46
+ metered: false,
47
+ online: false,
48
+ rtt: -1,
49
+ saveData: false,
50
+ type: 'unknown',
51
+ };
52
+ }
53
+ // Builds the default web backend over navigator.onLine, the Network Information API, and the window
54
+ // online/offline events. Degrades to online=true / type 'unknown' where the APIs are absent.
55
+ export function createWebConnectivityBackend() {
56
+ return {
57
+ getStatus(out) {
58
+ const nav = typeof navigator !== 'undefined' ? navigator : null;
59
+ out.online = nav?.onLine ?? true;
60
+ const conn = getWebConnection();
61
+ out.type = mapWebConnectionType(conn?.type);
62
+ out.downlink = typeof conn?.downlink === 'number' ? conn.downlink : -1;
63
+ out.downlinkMax = typeof conn?.downlinkMax === 'number' ? conn.downlinkMax : -1;
64
+ out.effectiveType = typeof conn?.effectiveType === 'string' ? conn.effectiveType : '';
65
+ out.rtt = typeof conn?.rtt === 'number' ? conn.rtt : -1;
66
+ out.saveData = conn?.saveData === true;
67
+ out.metered = out.saveData || out.type === 'cellular';
68
+ return out;
69
+ },
70
+ async detectReachability(options, out) {
71
+ if (typeof fetch === 'undefined') {
72
+ out.reachable = false;
73
+ out.latency = -1;
74
+ return out;
75
+ }
76
+ const timeout = options.timeout ?? 5000;
77
+ const controller = new AbortController();
78
+ const timerId = setTimeout(() => controller.abort(), timeout);
79
+ const combinedSignal = options.signal ? anyAbortSignal(options.signal, controller.signal) : controller.signal;
80
+ const start = Date.now();
81
+ try {
82
+ const response = await fetch(options.url, {
83
+ method: 'HEAD',
84
+ cache: 'no-store',
85
+ signal: combinedSignal,
86
+ });
87
+ clearTimeout(timerId);
88
+ out.reachable = response.ok;
89
+ out.latency = Date.now() - start;
90
+ }
91
+ catch {
92
+ clearTimeout(timerId);
93
+ out.reachable = false;
94
+ out.latency = -1;
95
+ }
96
+ return out;
97
+ },
98
+ subscribe(listener) {
99
+ if (typeof window === 'undefined')
100
+ return () => { };
101
+ window.addEventListener('online', listener);
102
+ window.addEventListener('offline', listener);
103
+ const conn = getWebConnection();
104
+ conn?.addEventListener?.('change', listener);
105
+ return () => {
106
+ window.removeEventListener('online', listener);
107
+ window.removeEventListener('offline', listener);
108
+ conn?.removeEventListener?.('change', listener);
109
+ };
110
+ },
111
+ };
112
+ }
113
+ // Stops delivery to `net` and forgets its subscription. Safe to call when not attached.
114
+ export function detachConnectivity(net) {
115
+ const unsubscribe = _subscriptions.get(net);
116
+ if (unsubscribe !== undefined) {
117
+ unsubscribe();
118
+ _subscriptions.delete(net);
119
+ }
120
+ }
121
+ // Performs a one-shot reachability probe against the given URL using the active backend's
122
+ // detectReachability, falling back to a fetch-based implementation when the backend does not provide
123
+ // one. Writes the result into `out` and returns it. Returns a sentinel on failure rather than throwing.
124
+ // NOTE: navigator.onLine reports an interface, not internet reachability. Use this function when you
125
+ // need to distinguish "a network interface is up" from "the internet is actually reachable."
126
+ export async function detectConnectivityReachability(options, out) {
127
+ const backend = getConnectivityBackend();
128
+ if (backend.detectReachability !== undefined) {
129
+ return backend.detectReachability(options, out);
130
+ }
131
+ // Fallback: use the web backend's implementation directly
132
+ if (_cachedWebBackend === null)
133
+ _cachedWebBackend = createWebConnectivityBackend();
134
+ const webBackend = _cachedWebBackend;
135
+ if (webBackend.detectReachability !== undefined) {
136
+ return webBackend.detectReachability(options, out);
137
+ }
138
+ out.reachable = false;
139
+ out.latency = -1;
140
+ return out;
141
+ }
142
+ // Releases `net` for garbage collection by detaching its backend subscription. The signals remain
143
+ // plain GC-managed memory afterward.
144
+ export function disposeConnectivity(net) {
145
+ detachConnectivity(net);
146
+ }
147
+ // The active network backend, or a lazily-created web default. There is always a backend.
148
+ export function getConnectivityBackend() {
149
+ if (_backend === null)
150
+ _backend = createWebConnectivityBackend();
151
+ return _backend;
152
+ }
153
+ // Fills `out` with the current connectivity snapshot and returns it.
154
+ export function getConnectivityStatus(out) {
155
+ return getConnectivityBackend().getStatus(out);
156
+ }
157
+ // Returns a diff of two status snapshots. Returns true if any field differs.
158
+ export function hasConnectivityStatusChanged(a, b) {
159
+ return (a.online !== b.online ||
160
+ a.type !== b.type ||
161
+ a.downlink !== b.downlink ||
162
+ a.downlinkMax !== b.downlinkMax ||
163
+ a.effectiveType !== b.effectiveType ||
164
+ a.rtt !== b.rtt ||
165
+ a.saveData !== b.saveData ||
166
+ a.metered !== b.metered);
167
+ }
168
+ // True when the connection is metered (cellular or save-data is set). Convenience over getConnectivityStatus.
169
+ export function isConnectivityMetered() {
170
+ return getConnectivityBackend().getStatus(_scratch).metered;
171
+ }
172
+ // True when the host currently reports connectivity. Convenience over getConnectivityStatus.
173
+ export function isConnectivityOnline() {
174
+ return getConnectivityBackend().getStatus(_scratch).online;
175
+ }
176
+ // True when the user or OS has requested reduced data usage. Convenience over getConnectivityStatus.
177
+ export function isConnectivitySaveDataEnabled() {
178
+ return getConnectivityBackend().getStatus(_scratch).saveData;
179
+ }
180
+ // Installs a native host network backend; pass null to fall back to the web default.
181
+ export function setConnectivityBackend(backend) {
182
+ _backend = backend;
183
+ }
184
+ let _backend = null;
185
+ const _scratch = createConnectivityStatus();
186
+ const _subscriptions = new WeakMap();
187
+ // Combines two AbortSignals: aborts when either fires.
188
+ function anyAbortSignal(a, b) {
189
+ if (typeof AbortSignal !== 'undefined' && 'any' in AbortSignal) {
190
+ return AbortSignal.any([a, b]);
191
+ }
192
+ const controller = new AbortController();
193
+ const onAbort = () => {
194
+ controller.abort();
195
+ a.removeEventListener('abort', onAbort);
196
+ b.removeEventListener('abort', onAbort);
197
+ };
198
+ a.addEventListener('abort', onAbort, { once: true });
199
+ b.addEventListener('abort', onAbort, { once: true });
200
+ return controller.signal;
201
+ }
202
+ function getWebConnection() {
203
+ if (typeof navigator === 'undefined')
204
+ return null;
205
+ const nav = navigator;
206
+ return nav.connection ?? null;
207
+ }
208
+ function mapWebConnectionType(type) {
209
+ switch (type) {
210
+ case 'bluetooth':
211
+ return 'bluetooth';
212
+ case 'cellular':
213
+ return 'cellular';
214
+ case 'ethernet':
215
+ return 'ethernet';
216
+ case 'none':
217
+ return 'none';
218
+ case 'other':
219
+ return 'other';
220
+ case 'vpn':
221
+ return 'vpn';
222
+ case 'wifi':
223
+ return 'wifi';
224
+ case 'wimax':
225
+ return 'wimax';
226
+ default:
227
+ return 'unknown';
228
+ }
229
+ }
230
+ let _cachedWebBackend = null;
231
+ //# sourceMappingURL=connectivity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connectivity.js","sourceRoot":"","sources":["../src/connectivity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './connectivity';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './connectivity';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/connectivity",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Connectivity status and online/offline change signals over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,351 @@
1
+ import { connectSignal } from '@flighthq/signals';
2
+ import type { ConnectivityBackend, ConnectivityReachability, ConnectivityStatus } from '@flighthq/types';
3
+
4
+ import {
5
+ attachConnectivity,
6
+ createConnectivity,
7
+ createConnectivityStatus,
8
+ createWebConnectivityBackend,
9
+ detachConnectivity,
10
+ detectConnectivityReachability,
11
+ disposeConnectivity,
12
+ getConnectivityBackend,
13
+ getConnectivityStatus,
14
+ hasConnectivityStatusChanged,
15
+ isConnectivityMetered,
16
+ isConnectivityOnline,
17
+ isConnectivitySaveDataEnabled,
18
+ setConnectivityBackend,
19
+ } from './connectivity';
20
+
21
+ function fakeBackend(
22
+ overrides?: Partial<ConnectivityStatus>,
23
+ ): ConnectivityBackend & { status: ConnectivityStatus; fire: () => void } {
24
+ let listener: (() => void) | null = null;
25
+ const status: ConnectivityStatus = {
26
+ downlink: 10,
27
+ downlinkMax: 100,
28
+ effectiveType: '4g',
29
+ metered: false,
30
+ online: true,
31
+ rtt: 50,
32
+ saveData: false,
33
+ type: 'wifi',
34
+ ...overrides,
35
+ };
36
+ return {
37
+ status,
38
+ getStatus(out) {
39
+ out.online = status.online;
40
+ out.type = status.type;
41
+ out.downlink = status.downlink;
42
+ out.downlinkMax = status.downlinkMax;
43
+ out.effectiveType = status.effectiveType;
44
+ out.rtt = status.rtt;
45
+ out.saveData = status.saveData;
46
+ out.metered = status.metered;
47
+ return out;
48
+ },
49
+ subscribe(l) {
50
+ listener = l;
51
+ return () => {
52
+ listener = null;
53
+ };
54
+ },
55
+ fire() {
56
+ listener?.();
57
+ },
58
+ };
59
+ }
60
+
61
+ afterEach(() => setConnectivityBackend(null));
62
+
63
+ describe('attachConnectivity', () => {
64
+ it('emits onChange and onOffline on transition to offline', () => {
65
+ const backend = fakeBackend();
66
+ setConnectivityBackend(backend);
67
+ const net = createConnectivity();
68
+ let changes = 0;
69
+ let offline = 0;
70
+ connectSignal(net.onChange, () => changes++);
71
+ connectSignal(net.onOffline, () => offline++);
72
+ attachConnectivity(net);
73
+ backend.status.online = false;
74
+ (backend as ReturnType<typeof fakeBackend>).fire();
75
+ expect(changes).toBe(1);
76
+ expect(offline).toBe(1);
77
+ });
78
+
79
+ it('emits onOnline on transition to online', () => {
80
+ const backend = fakeBackend({ online: false });
81
+ setConnectivityBackend(backend);
82
+ const net = createConnectivity();
83
+ let online = 0;
84
+ connectSignal(net.onOnline, () => online++);
85
+ attachConnectivity(net);
86
+ backend.status.online = true;
87
+ (backend as ReturnType<typeof fakeBackend>).fire();
88
+ expect(online).toBe(1);
89
+ });
90
+
91
+ it('emits onConnectionTypeChange on type transition', () => {
92
+ const backend = fakeBackend();
93
+ setConnectivityBackend(backend);
94
+ const net = createConnectivity();
95
+ const types: string[] = [];
96
+ connectSignal(net.onConnectionTypeChange, (t) => types.push(t));
97
+ attachConnectivity(net);
98
+ backend.status.type = 'cellular';
99
+ (backend as ReturnType<typeof fakeBackend>).fire();
100
+ expect(types).toEqual(['cellular']);
101
+ });
102
+
103
+ it('does not emit onConnectionTypeChange when type is unchanged', () => {
104
+ const backend = fakeBackend();
105
+ setConnectivityBackend(backend);
106
+ const net = createConnectivity();
107
+ let count = 0;
108
+ connectSignal(net.onConnectionTypeChange, () => count++);
109
+ attachConnectivity(net);
110
+ // fire without changing type
111
+ (backend as ReturnType<typeof fakeBackend>).fire();
112
+ expect(count).toBe(0);
113
+ });
114
+
115
+ it('emits onMeteredChange on metered transition', () => {
116
+ const backend = fakeBackend();
117
+ setConnectivityBackend(backend);
118
+ const net = createConnectivity();
119
+ const metered: boolean[] = [];
120
+ connectSignal(net.onMeteredChange, (m) => metered.push(m));
121
+ attachConnectivity(net);
122
+ backend.status.metered = true;
123
+ (backend as ReturnType<typeof fakeBackend>).fire();
124
+ expect(metered).toEqual([true]);
125
+ });
126
+
127
+ it('is idempotent — replaces prior subscription', () => {
128
+ const backend = fakeBackend();
129
+ setConnectivityBackend(backend);
130
+ const net = createConnectivity();
131
+ let changes = 0;
132
+ connectSignal(net.onChange, () => changes++);
133
+ attachConnectivity(net);
134
+ attachConnectivity(net);
135
+ (backend as ReturnType<typeof fakeBackend>).fire();
136
+ expect(changes).toBe(1);
137
+ });
138
+ });
139
+
140
+ describe('createConnectivity', () => {
141
+ it('creates an entity with five signals', () => {
142
+ const net = createConnectivity();
143
+ expect(net.onChange).toBeDefined();
144
+ expect(net.onConnectionTypeChange).toBeDefined();
145
+ expect(net.onMeteredChange).toBeDefined();
146
+ expect(net.onOnline).toBeDefined();
147
+ expect(net.onOffline).toBeDefined();
148
+ });
149
+ });
150
+
151
+ describe('createConnectivityStatus', () => {
152
+ it('allocates a zeroed status with all fields at sentinel values', () => {
153
+ expect(createConnectivityStatus()).toEqual({
154
+ downlink: -1,
155
+ downlinkMax: -1,
156
+ effectiveType: '',
157
+ metered: false,
158
+ online: false,
159
+ rtt: -1,
160
+ saveData: false,
161
+ type: 'unknown',
162
+ });
163
+ });
164
+ });
165
+
166
+ describe('createWebConnectivityBackend', () => {
167
+ it('reads a status without throwing', () => {
168
+ const out = createConnectivityStatus();
169
+ const result = createWebConnectivityBackend().getStatus(out);
170
+ expect(typeof result.online).toBe('boolean');
171
+ expect(typeof result.metered).toBe('boolean');
172
+ expect(typeof result.saveData).toBe('boolean');
173
+ });
174
+
175
+ it('returns sentinel rtt and downlinkMax when connection is absent', () => {
176
+ const out = createConnectivityStatus();
177
+ createWebConnectivityBackend().getStatus(out);
178
+ // In jsdom, navigator.connection is absent so rtt/downlinkMax default to -1
179
+ expect(out.rtt).toBe(-1);
180
+ expect(out.downlinkMax).toBe(-1);
181
+ });
182
+ });
183
+
184
+ describe('detachConnectivity', () => {
185
+ it('stops further delivery', () => {
186
+ const backend = fakeBackend();
187
+ setConnectivityBackend(backend);
188
+ const net = createConnectivity();
189
+ let changes = 0;
190
+ connectSignal(net.onChange, () => changes++);
191
+ attachConnectivity(net);
192
+ detachConnectivity(net);
193
+ (backend as ReturnType<typeof fakeBackend>).fire();
194
+ expect(changes).toBe(0);
195
+ });
196
+
197
+ it('is safe to call when not attached', () => {
198
+ const net = createConnectivity();
199
+ expect(() => detachConnectivity(net)).not.toThrow();
200
+ });
201
+ });
202
+
203
+ describe('detectConnectivityReachability', () => {
204
+ it('returns a sentinel when fetch is unavailable (SSR/jsdom guard)', async () => {
205
+ // The web backend's detectReachability returns a sentinel in jsdom where fetch is absent or HEAD
206
+ // requests fail. We install a backend without detectReachability to test the fallback path.
207
+ setConnectivityBackend(fakeBackend());
208
+ const out: ConnectivityReachability = { latency: 0, reachable: true };
209
+ // fetch is undefined in vitest/jsdom by default — expect sentinel
210
+ const result = await detectConnectivityReachability({ url: 'https://example.com' }, out);
211
+ expect(result).toBe(out);
212
+ // In jsdom fetch may not exist or may fail; either way we get reachable=false or a live result
213
+ expect(typeof result.reachable).toBe('boolean');
214
+ });
215
+
216
+ it('uses the backend detectReachability when available', async () => {
217
+ const probeBackend: ConnectivityBackend = {
218
+ ...fakeBackend(),
219
+ async detectReachability(_opts, out) {
220
+ out.reachable = true;
221
+ out.latency = 42;
222
+ return out;
223
+ },
224
+ };
225
+ setConnectivityBackend(probeBackend);
226
+ const out: ConnectivityReachability = { latency: 0, reachable: false };
227
+ const result = await detectConnectivityReachability({ url: 'https://example.com' }, out);
228
+ expect(result.reachable).toBe(true);
229
+ expect(result.latency).toBe(42);
230
+ });
231
+ });
232
+
233
+ describe('disposeConnectivity', () => {
234
+ it('detaches the subscription', () => {
235
+ const backend = fakeBackend();
236
+ setConnectivityBackend(backend);
237
+ const net = createConnectivity();
238
+ attachConnectivity(net);
239
+ expect(() => disposeConnectivity(net)).not.toThrow();
240
+ });
241
+
242
+ it('is safe to call when not attached', () => {
243
+ const net = createConnectivity();
244
+ expect(() => disposeConnectivity(net)).not.toThrow();
245
+ });
246
+ });
247
+
248
+ describe('getConnectivityBackend', () => {
249
+ it('falls back to a web backend', () => {
250
+ expect(getConnectivityBackend()).not.toBeNull();
251
+ });
252
+ });
253
+
254
+ describe('getConnectivityStatus', () => {
255
+ it('fills the out parameter from the backend', () => {
256
+ setConnectivityBackend(fakeBackend());
257
+ const out = createConnectivityStatus();
258
+ expect(getConnectivityStatus(out)).toBe(out);
259
+ expect(out.type).toBe('wifi');
260
+ expect(out.rtt).toBe(50);
261
+ expect(out.downlinkMax).toBe(100);
262
+ });
263
+ });
264
+
265
+ describe('hasConnectivityStatusChanged', () => {
266
+ it('returns false for equal statuses', () => {
267
+ const a = createConnectivityStatus();
268
+ const b = createConnectivityStatus();
269
+ expect(hasConnectivityStatusChanged(a, b)).toBe(false);
270
+ });
271
+
272
+ it('returns true when online differs', () => {
273
+ const a = { ...createConnectivityStatus(), online: true };
274
+ const b = createConnectivityStatus();
275
+ expect(hasConnectivityStatusChanged(a, b)).toBe(true);
276
+ });
277
+
278
+ it('returns true when type differs', () => {
279
+ const a = { ...createConnectivityStatus(), type: 'wifi' as const };
280
+ const b = createConnectivityStatus();
281
+ expect(hasConnectivityStatusChanged(a, b)).toBe(true);
282
+ });
283
+
284
+ it('returns true when rtt differs', () => {
285
+ const a = { ...createConnectivityStatus(), rtt: 30 };
286
+ const b = createConnectivityStatus();
287
+ expect(hasConnectivityStatusChanged(a, b)).toBe(true);
288
+ });
289
+
290
+ it('returns true when saveData differs', () => {
291
+ const a = { ...createConnectivityStatus(), saveData: true };
292
+ const b = createConnectivityStatus();
293
+ expect(hasConnectivityStatusChanged(a, b)).toBe(true);
294
+ });
295
+
296
+ it('returns true when metered differs', () => {
297
+ const a = { ...createConnectivityStatus(), metered: true };
298
+ const b = createConnectivityStatus();
299
+ expect(hasConnectivityStatusChanged(a, b)).toBe(true);
300
+ });
301
+
302
+ it('is alias-safe — same object returns false', () => {
303
+ const s = createConnectivityStatus();
304
+ expect(hasConnectivityStatusChanged(s, s)).toBe(false);
305
+ });
306
+ });
307
+
308
+ describe('isConnectivityMetered', () => {
309
+ it('returns false for a non-metered connection', () => {
310
+ setConnectivityBackend(fakeBackend({ metered: false }));
311
+ expect(isConnectivityMetered()).toBe(false);
312
+ });
313
+
314
+ it('returns true for a metered connection', () => {
315
+ setConnectivityBackend(fakeBackend({ metered: true }));
316
+ expect(isConnectivityMetered()).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe('isConnectivityOnline', () => {
321
+ it('reflects the backend online flag', () => {
322
+ const backend = fakeBackend({ online: false });
323
+ setConnectivityBackend(backend);
324
+ expect(isConnectivityOnline()).toBe(false);
325
+ });
326
+
327
+ it('returns true when the backend is online', () => {
328
+ setConnectivityBackend(fakeBackend({ online: true }));
329
+ expect(isConnectivityOnline()).toBe(true);
330
+ });
331
+ });
332
+
333
+ describe('isConnectivitySaveDataEnabled', () => {
334
+ it('returns false when saveData is off', () => {
335
+ setConnectivityBackend(fakeBackend({ saveData: false }));
336
+ expect(isConnectivitySaveDataEnabled()).toBe(false);
337
+ });
338
+
339
+ it('returns true when saveData is on', () => {
340
+ setConnectivityBackend(fakeBackend({ saveData: true }));
341
+ expect(isConnectivitySaveDataEnabled()).toBe(true);
342
+ });
343
+ });
344
+
345
+ describe('setConnectivityBackend', () => {
346
+ it('clears back to the web fallback when passed null', () => {
347
+ setConnectivityBackend(fakeBackend());
348
+ setConnectivityBackend(null);
349
+ expect(getConnectivityBackend()).not.toBeNull();
350
+ });
351
+ });