Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add useNetworkStatus #117

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from "./AnimationFrames/index.js";
export * from "./useIntersectionObserver/index.js";
export * from "./IsFocusWithin/index.js";
export * from "./FiniteStateMachine/index.js";
export * from "./useNetworkStatus/index.js";
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/useNetworkStatus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useNetworkStatus.svelte.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { browser } from "$lib/internal/utils/browser.js";
import { addEventListener } from "$lib/internal/utils/event.js";
import { Previous } from "$lib/utilities/index.js";

/**
* @desc The `NetworkInformation` interface of the Network Information API
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation
*/
type NetworkInformation = {
readonly downlink: number;
readonly downlinkMax: number;
readonly effectiveType: "slow-2g" | "2g" | "3g" | "4g";
readonly rtt: number;
readonly saveData: boolean;
readonly type:
| "bluetooth"
| "cellular"
| "ethernet"
| "none"
| "wifi"
| "wimax"
| "other"
| "unknown";
} & EventTarget;

type NavigatorWithConnection = Navigator & { connection: NetworkInformation };

interface NetworkStatus {
/**
* @desc Returns the online status of the browser.
* @see https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine
*/
online: boolean;
/**
* @desc The {Date} object pointing to the moment when state update occurred.
*/
updatedAt: Date;
/**
* @desc Effective bandwidth estimate in megabits per second, rounded to the
* nearest multiple of 25 kilobits per seconds.
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlink
*/
downlink?: NetworkInformation["downlink"];
/**
* @desc Maximum downlink speed, in megabits per second (Mbps), for the
* underlying connection technology
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/downlinkMax
*/
downlinkMax?: NetworkInformation["downlinkMax"];
/**
* @desc Effective type of the connection meaning one of 'slow-2g', '2g', '3g', or '4g'.
* This value is determined using a combination of recently observed round-trip time
* and downlink values.
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType
*/
effectiveType?: NetworkInformation["effectiveType"];
/**
* @desc Estimated effective round-trip time of the current connection, rounded
* to the nearest multiple of 25 milliseconds
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/rtt
*/
rtt?: NetworkInformation["rtt"];
/**
* @desc {true} if the user has set a reduced data usage option on the user agent.
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData
*/
saveData?: NetworkInformation["saveData"];
/**
* @desc The type of connection a device is using to communicate with the network.
* It will be one of the following values:
* - bluetooth
* - cellular
* - ethernet
* - none
* - wifi
* - wimax
* - other
* - unknown
* @see https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/type
*/
type?: NetworkInformation["type"];
}

/**
* Tracks the state of browser's network connection.
* @returns null if the function is not run in the browser
*/
export function useNetworkStatus() {
michal-weglarz marked this conversation as resolved.
Show resolved Hide resolved
console.log("browser", browser);
michal-weglarz marked this conversation as resolved.
Show resolved Hide resolved
if (!browser) {
return {
get current() {
return null;
},
get previous() {
return null;
},
};
}
const navigator = window.navigator;
const connection =
"connection" in navigator ? (navigator as NavigatorWithConnection).connection : undefined;
const getConnectionStatus = (): NetworkStatus => {
return {
online: navigator.onLine,
updatedAt: new Date(),
downlink: connection?.downlink,
downlinkMax: connection?.downlinkMax,
effectiveType: connection?.effectiveType,
rtt: connection?.rtt,
saveData: connection?.saveData,
type: connection?.type,
};
};

let current = $state(getConnectionStatus());
const previous = new Previous(() => current);
michal-weglarz marked this conversation as resolved.
Show resolved Hide resolved
const handleStatusChange = () => {
current = getConnectionStatus();
};

$effect(() => {
const callbacks: VoidFunction[] = [];

// The connection event handler also manages online and offline states.
if (connection) {
callbacks.push(addEventListener(connection, "change", handleStatusChange, { passive: true }));
} else {
callbacks.push(addEventListener(window, "online", handleStatusChange, { passive: true }));
callbacks.push(addEventListener(window, "offline", handleStatusChange, { passive: true }));
}

return () => {
callbacks.forEach((c) => c());
};
});

return {
get current() {
return current;
},
get previous() {
return previous.current;
},
};
}
94 changes: 94 additions & 0 deletions sites/docs/content/utilities/use-network-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
title: useNetworkStatus
description: Watch for network status changes.
category: Browser
---

<script>
import Demo from '$lib/components/demos/use-network-status.svelte';
import { Callout } from '$lib/components'
</script>

## Demo

To see it in action, try disabling and then re-enabling your Internet connection.
If you're using Google Chrome, you can also [artificially throttle the network](https://developer.chrome.com/docs/devtools/settings/throttling) to test its behavior under different conditions.

<Demo />

## Usage

You can use `useNetworkStatus()` to retrieve both current and previous network status.
It must be used within the `browser` context, otherwise it will return `null`.

```svelte
<script lang="ts">
import { useNetworkStatus } from "runed";
import { toast } from "svelte-sonner";

const networkStatus = useNetworkStatus();

$effect(() => {
if (!networkStatus.current) {
return;
}
if (networkStatus.current.online === false) {
toast.error("No internet connection.");
}
if (networkStatus.current.effectiveType === "2g") {
toast.warning("You are experiencing a slow connection.");
}
if (networkStatus.current.online === true && networkStatus.previous?.online === false) {
toast.success("You are back online!");
}
});
</script>


{#if networkStatus.current}
<p>online: {networkStatus.current.online}</p>
{/if}
```

### Current

You can get the current status by calling the `current` method.


```ts
const networkStatus = useNetworkStatus();

networkStatus.current;
```

### Previous

You can get the previous status by calling the `previous` method.
It defaults to `undefined` if the network status hasn't been updated since the component mounted.

```ts
const networkStatus = useNetworkStatus();

networkStatus.previous;
```

## Reference

The returned status always includes `online` and `updatedAt`.
Other properties are returned based on the [NetworkInformation](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation#instance_properties) interface and depend on [your browser's compatibility](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation#browser_compatibility).

```typescript
interface NetworkStatus {
online: boolean;
updatedAt: Date;
downlink?: number;
downlinkMax?: number;
effectiveType?: "slow-2g" | "2g" | "3g" | "4g";
rtt?: number;
saveData?: boolean;
type?: "bluetooth" | "cellular" | "ethernet" | "none" | "wifi" | "wimax" | "other" | "unknown";
}
```



54 changes: 54 additions & 0 deletions sites/docs/src/lib/components/demos/use-network-status.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<script lang="ts">
import { useNetworkStatus } from "runed";
import { toast } from "svelte-sonner";
import DemoContainer from "$lib/components/demo-container.svelte";

const networkStatus = useNetworkStatus();

const timeOffline = $derived(() => {
if (networkStatus && networkStatus.previous) {
const now = networkStatus.current.updatedAt;
const prev = networkStatus.previous.updatedAt;
if (!now || !prev) {
return 0;
}
const differenceMs = now.getTime() - prev.getTime();
if (differenceMs < 0) {
return 0;
}
const differenceSeconds = differenceMs / 1000;
return Math.round(differenceSeconds);
}
return 0;
});

$effect(() => {
if (!networkStatus.current) {
return;
}

if (networkStatus.current.online === false) {
toast.error("No internet connection. Reconnect to continue using the app.");
}
if (
networkStatus.current.effectiveType === "3g" ||
networkStatus.current.effectiveType === "2g" ||
networkStatus.current.effectiveType === "slow-2g"
) {
toast.warning(
"You are experiencing a slow connection. Some features may take longer to load."
);
}
if (networkStatus.current.online === true && networkStatus.previous?.online === false) {
toast.success(
`You are back online! Catch up with what you missed during your ${timeOffline()} seconds offline.`
);
}
});
</script>

<DemoContainer>
{#if networkStatus.current}
<pre>{JSON.stringify(networkStatus.current, null, 2)}</pre>
{/if}
</DemoContainer>