Skip to content

Commit

Permalink
feat: ported injectNetwork() to Angular (#186)
Browse files Browse the repository at this point in the history
* feat: ported injectNetwork() to Angular

* Update libs/ngxtension/inject-network/src/inject-network.ts

Co-authored-by: Chau Tran <[email protected]>

* Update libs/ngxtension/inject-network/src/inject-network.ts

Co-authored-by: Chau Tran <[email protected]>

* Update libs/ngxtension/inject-network/src/inject-network.ts

Co-authored-by: Chau Tran <[email protected]>

* Update libs/ngxtension/inject-network/src/inject-network.ts

Co-authored-by: Chau Tran <[email protected]>

* Update libs/ngxtension/inject-network/src/inject-network.ts

Co-authored-by: Chau Tran <[email protected]>

* Lint fix

* adjust implementation

---------

Co-authored-by: Chau Tran <[email protected]>
  • Loading branch information
fiorelozere and nartc authored Dec 6, 2023
1 parent 437aa2c commit 684a33a
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 0 deletions.
60 changes: 60 additions & 0 deletions docs/src/content/docs/utilities/Injectors/inject-network.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: injectNetwork
description: ngxtension/inject-network
badge: stable
contributor: fiorelozere
---

This injector is useful for tracking the current network state of the user. It provides information about the system's connection type, such as 'wifi' or 'cellular'. This utility, along with a singular property added to the Navigator interface (Navigator.connection), allows for the identification of the general type of network connection a system is using. This functionality is particularly useful for choosing between high definition or low definition content depending on the user's network connection.

```ts
import { injectNetwork } from 'ngxtension/inject-network';
```

## Usage

`injectNetwork` accepts an optional parameter `options` which can include a custom `window` and an `Injector` instance that are both optional. The `window` parameter is particularly useful for testing scenarios or when needing to track the network state of an iframe. The `injector` allows for dependency injection, providing more flexibility and facilitating testable code by decoupling from the global state or context.

```ts
const networkState = injectNetwork();

effect(() => {
console.log(this.network.type());
console.log(this.network.downlink());
console.log(this.network.downlinkMax());
console.log(this.network.effectiveType());
console.log(this.network.rtt());
console.log(this.network.saveData());
console.log(this.network.online());
console.log(this.network.offlineAt());
console.log(this.network.onlineAt());
console.log(this.network.supported());
});
```

## API

```ts
function injectNetwork(options?: InjectNetworkOptions): Readonly<NetworkState>;
```

### Parameters

- `options` (optional): An object that can have the following properties:
- `window`: A custom `Window` instance, defaulting to the global `window` object.
- `injector`: An `Injector` instance for Angular's dependency injection.

### Returns

A readonly object with the following properties:

- `supported`: A signal that emits `true` if the browser supports the Network Information API, otherwise `false`.
- `online`: A signal that emits `true` if the user is online, otherwise `false`.
- `offlineAt`: A signal that emits the time since the user was last connected.
- `onlineAt`: A signal that emits the time since the user was last disconnected.
- `downlink`: A signal that emits the download speed in Mbps.
- `downlinkMax`: A signal that emits the max reachable download speed in Mbps.
- `effectiveType`: A signal that emits the detected effective speed type.
- `rtt`: A signal that emits the estimated effective round-trip time of the current connection.
- `saveData`: A signal that emits `true` if the user activated data saver mode, otherwise `false`.
- `type`: A signal that emits the detected connection/network type.
3 changes: 3 additions & 0 deletions libs/ngxtension/inject-network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ngxtension/inject-network

Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/inject-network`.
5 changes: 5 additions & 0 deletions libs/ngxtension/inject-network/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
}
}
33 changes: 33 additions & 0 deletions libs/ngxtension/inject-network/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "ngxtension/inject-network",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "library",
"sourceRoot": "libs/ngxtension/inject-network/src",
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ngxtension/jest.config.ts",
"testPathPattern": ["inject-network"],
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"libs/ngxtension/inject-network/**/*.ts",
"libs/ngxtension/inject-network/**/*.html"
]
}
}
}
}
1 change: 1 addition & 0 deletions libs/ngxtension/inject-network/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './inject-network';
66 changes: 66 additions & 0 deletions libs/ngxtension/inject-network/src/inject-network.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { injectNetwork } from './inject-network';

describe(injectNetwork.name, () => {
@Component({
standalone: true,
template: `
{{ networkState.saveData() }}
{{ networkState.type() }}
{{ networkState.downlink() }}
{{ networkState.downlinkMax() }}
{{ networkState.effectiveType() }}
{{ networkState.rtt() }}
{{ networkState.online() }}
{{ networkState.onlineAt() }}
{{ networkState.offlineAt() }}
`,
})
class Test {
networkState = injectNetwork();
}

function setup() {
const fixture = TestBed.createComponent(Test);
fixture.detectChanges();
return fixture.componentInstance;
}

function triggerOnlineEvent() {
// Change the visibility state
Object.defineProperty(navigator, 'onLine', {
writable: true,
configurable: true,
value: true,
});

// Dispatch the event
const event = new Event('online');
window.dispatchEvent(event);
}

function triggerOfflineEvent() {
// Mock navigator.onLine to return false
Object.defineProperty(navigator, 'onLine', {
value: false,
writable: true,
});

// Dispatch the offline event
const offlineEvent = new Event('offline');
window.dispatchEvent(offlineEvent);
}

it('should handle online state', () => {
const cmp = setup();
triggerOnlineEvent();
expect(cmp.networkState.online()).toEqual(true);
});

it('should handle offline state', () => {
const cmp = setup();
triggerOfflineEvent();
expect(cmp.networkState.online()).toEqual(false);
});
});
177 changes: 177 additions & 0 deletions libs/ngxtension/inject-network/src/inject-network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { DOCUMENT } from '@angular/common';
import { inject, signal, type Injector, type Signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { assertInjector } from 'ngxtension/assert-injector';
import { fromEvent, map, merge, startWith } from 'rxjs';

// Ported from https://vueuse.org/core/useNetwork/

export type NetworkType =
| 'bluetooth'
| 'cellular'
| 'ethernet'
| 'none'
| 'wifi'
| 'wimax'
| 'other'
| 'unknown';

export type NetworkEffectiveType = 'slow-2g' | '2g' | '3g' | '4g' | '5g';

export interface NetworkState {
supported: Signal<boolean>;
online: Signal<boolean>;
/**
* The time since the user was last connected.
*/
offlineAt: Signal<number | undefined>;
/**
* At this time, if the user is offline and reconnects
*/
onlineAt: Signal<number | undefined>;
/**
* The download speed in Mbps.
*/
downlink: Signal<number | undefined>;
/**
* The max reachable download speed in Mbps.
*/
downlinkMax: Signal<number | undefined>;
/**
* The detected effective speed type.
*/
effectiveType: Signal<NetworkEffectiveType | undefined>;
/**
* The estimated effective round-trip time of the current connection.
*/
rtt: Signal<number | undefined>;
/**
* If the user activated data saver mode.
*/
saveData: Signal<boolean | undefined>;
/**
* The detected connection/network type.
*/
type: Signal<NetworkType>;
}

export interface InjectNetworkOptions {
injector?: Injector;
window?: Window;
}

/**
* This injector is useful for tracking the current network state of the user. It provides information about the system's connection type, such as 'wifi' or 'cellular'. This utility, along with a singular property added to the Navigator interface (Navigator.connection), allows for the identification of the general type of network connection a system is using. This functionality is particularly useful for choosing between high definition or low definition content depending on the user's network connection.
*
* @example
* ```ts
* const network = injectNetwork();
* effect(() => {
* console.log(this.network.type());
* console.log(this.network.downlink());
* console.log(this.network.downlinkMax());
* console.log(this.network.effectiveType());
* console.log(this.network.rtt());
* console.log(this.network.saveData());
* console.log(this.network.online());
* console.log(this.network.offlineAt());
* console.log(this.network.onlineAt());
* console.log(this.network.supported());
* });
* ```
*
* @param options An optional object with the following properties:
* - `window`: (Optional) Specifies a custom `Window` instance. This is useful when working with iframes or in testing environments where the global `window` might not be appropriate.
* - `injector`: (Optional) Specifies a custom `Injector` instance for dependency injection. This allows for more flexible and testable code by decoupling from a global state or context.
*
* @returns A readonly object with the following properties:
* - `supported`: A signal that emits `true` if the browser supports the Network Information API, otherwise `false`.
* - `online`: A signal that emits `true` if the user is online, otherwise `false`.
* - `offlineAt`: A signal that emits the time since the user was last connected.
* - `onlineAt`: A signal that emits the time since the user was last disconnected.
* - `downlink`: A signal that emits the download speed in Mbps.
* - `downlinkMax`: A signal that emits the max reachable download speed in Mbps.
* - `effectiveType`: A signal that emits the detected effective speed type.
* - `rtt`: A signal that emits the estimated effective round-trip time of the current connection.
* - `saveData`: A signal that emits `true` if the user activated data saver mode, otherwise `false`.
* - `type`: A signal that emits the detected connection/network type.
*/
export function injectNetwork({
injector,
window: customWindow,
}: InjectNetworkOptions = {}): Readonly<NetworkState> {
return assertInjector(injectNetwork, injector, () => {
const window: Window = customWindow ?? inject(DOCUMENT).defaultView!;
const navigator = window?.navigator;

const supported = signal(
window?.navigator && 'connection' in window.navigator
);

const online = signal(true);
const saveData = signal(false);
const offlineAt = signal<number | undefined>(undefined);
const onlineAt = signal<number | undefined>(undefined);
const downlink = signal<number | undefined>(undefined);
const downlinkMax = signal<number | undefined>(undefined);
const rtt = signal<number | undefined>(undefined);
const effectiveType = signal<NetworkEffectiveType | undefined>(undefined);
const type = signal<NetworkType>('unknown');

const connection = supported() && (navigator as any).connection;

const updateNetworkInformation = () => {
if (!navigator) return;

offlineAt.set(online() ? undefined : Date.now());
onlineAt.set(online() ? Date.now() : undefined);

if (connection) {
downlink.set(connection.downlink);
downlinkMax.set(connection.downlinkMax);
effectiveType.set(connection.effectiveType);
rtt.set(connection.rtt);
saveData.set(connection.saveData);
type.set(connection.type);
}
};

if (window) {
merge(
fromEvent(window, 'online').pipe(map(() => true)),
fromEvent(window, 'offline').pipe(map(() => false))
)
.pipe(takeUntilDestroyed())
.subscribe((isOnline) => {
online.set(isOnline);
if (isOnline) {
onlineAt.set(Date.now());
} else {
offlineAt.set(Date.now());
}
});
}

if (connection) {
fromEvent(connection, 'change')
.pipe(
startWith(null), // we need to start with null to trigger the first update
takeUntilDestroyed()
)
.subscribe(() => updateNetworkInformation());
}

return {
supported: supported.asReadonly(),
online: online.asReadonly(),
saveData: saveData.asReadonly(),
offlineAt: offlineAt.asReadonly(),
onlineAt: onlineAt.asReadonly(),
downlink: downlink.asReadonly(),
downlinkMax: downlinkMax.asReadonly(),
effectiveType: effectiveType.asReadonly(),
rtt: rtt.asReadonly(),
type: type.asReadonly(),
};
});
}
3 changes: 3 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
"libs/ngxtension/inject-is-intersecting/src/index.ts"
],
"ngxtension/inject-lazy": ["libs/ngxtension/inject-lazy/src/index.ts"],
"ngxtension/inject-network": [
"libs/ngxtension/inject-network/src/index.ts"
],
"ngxtension/inject-params": [
"libs/ngxtension/inject-params/src/index.ts"
],
Expand Down

0 comments on commit 684a33a

Please sign in to comment.