├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .prettierrc.yaml
├── README.md
├── babel.config.js
├── index.js
├── package.json
├── src
├── cpu.js
├── memory.js
├── network.js
└── savedata.js
├── test
├── cpu.spec.js
├── memory.spec.js
├── network.spec.js
└── savedata.spec.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | insert_final_newline = true
6 | indent_style = tabs
7 | indent_size = 2
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 |
11 | [test/**/expected.css]
12 | insert_final_newline = false
13 |
14 | [{package.json,.travis.yml,.eslintrc.json}]
15 | indent_style = space
16 | indent_size = 2
17 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "rules": {
4 | "indent": ["error", "tab", { "SwitchCase": 1 }],
5 | "semi": [2, "always"],
6 | "keyword-spacing": [2, { "before": true, "after": true }],
7 | "space-before-blocks": [2, "always"],
8 | "no-mixed-spaces-and-tabs": [2, "smart-tabs"],
9 | "no-cond-assign": 0,
10 | "no-unused-vars": 2,
11 | "object-shorthand": [2, "always"],
12 | "no-const-assign": 2,
13 | "no-class-assign": 2,
14 | "no-this-before-super": 2,
15 | "no-var": 2,
16 | "no-unreachable": 2,
17 | "valid-typeof": 2,
18 | "quote-props": [2, "as-needed"],
19 | "one-var": [2, "never"],
20 | "prefer-arrow-callback": 2,
21 | "prefer-const": [2, { "destructuring": "all" }],
22 | "arrow-spacing": 2,
23 | "no-inner-declarations": 0
24 | },
25 |
26 | "env": {
27 | "es6": true,
28 | "browser": true,
29 | "node": true,
30 | "jest": true
31 | },
32 | "extends": [
33 | "eslint:recommended",
34 | "plugin:import/errors",
35 | "plugin:import/warnings"
36 | ],
37 | "plugins": ["import"],
38 | "parserOptions": {
39 | "ecmaVersion": 9,
40 | "sourceType": "module",
41 | "ecmaFeatures": {
42 | "experimentalObjectRestSpread": true
43 | }
44 | },
45 | "settings": {
46 | "import/resolver": {
47 | "node": {
48 | "extensions": [".js"]
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # macOS stuff
3 | .DS_Store
4 | .AppleDouble
5 | .LSOverride
6 |
7 | # duh
8 | /node_modules/
9 |
10 | # logs
11 | logs
12 | *.log
13 | npm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 |
17 | # testy stuff
18 | coverage
19 |
20 | # editor
21 | .vscode/*
22 | !.vscode/settings.json
23 | !.vscode/tasks.json
24 | !.vscode/launch.json
25 | !.vscode/extensions.json
26 |
--------------------------------------------------------------------------------
/.prettierrc.yaml:
--------------------------------------------------------------------------------
1 | useTabs: true
2 | singleQuote: true
3 | trailingComma: 'es5'
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # svelte-adaptive-sensors
2 |
3 | Sensors to help you deliver adaptive sensors to users depending on their network-type, memory, cpu, and saveData settings. A svelte version of [`react-adaptive-hooks`](https://github.com/GoogleChromeLabs/react-adaptive-hooks/) although there are very few differences between the two libraries currently.
4 |
5 | This library makes it easier to get information about a user's device, settings and network and alter your app's behaviour using these metrics.
6 |
7 | [Check it out liiiiiiive.](https://svelte.dev/repl/e00898da2de14e40a337a6ea5ac060f8?version=3.14.0)
8 |
9 | Currently 4 APIs are supported:
10 |
11 | - [Network via effective connection type](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/effectiveType)
12 | - [Data Saver preferences](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation/saveData)
13 | - [Device memory](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/deviceMemory)
14 | - [Logical CPU cores](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorConcurrentHardware/hardwareConcurrency)
15 |
16 | ## Install
17 |
18 | With `npm`:
19 |
20 | ```bash
21 | npm install --save-dev svelte-adaptive-sensors
22 | ```
23 |
24 | Or `yarn`:
25 |
26 | ```bash
27 | yarn add --dev svelte-adaptive-sensors
28 | ```
29 |
30 | ## Use
31 |
32 | Import them:
33 |
34 | ```js
35 | import {
36 | getCpuInfo,
37 | getMemoryInfo,
38 | getNetworkInfo,
39 | getSaveDataInfo,
40 | } from 'svelte-adaptive-sensors';
41 | ```
42 |
43 | And then use them.
44 |
45 | ## API
46 |
47 | All functions (or stores, in the case of `getNetworkInfo`) return an object with a `supported` property. This value is `false` if the API is not supported and `true` if it is.
48 |
49 | ### `getCpuInfo`
50 |
51 | A simple function that returns information about a user's logical processor cores using the `navigator.hardwareConcurrency` API.
52 |
53 | This value is static and will never change. User don't routinely swap out their CPU when using an app and if they do then I wnat to hear about it.
54 |
55 | ```ts
56 | getCpuInfo() = {
57 | supported: Boolean,
58 | processors:? Number
59 | };
60 | ```
61 |
62 | If `supported` is `false` then the `processors` property will not be present.
63 |
64 | ```svelte
65 |
70 |
71 | {#if supported && processors > 4}
72 |
73 | {:else}
74 |
75 | {/if}
76 | ```
77 |
78 | ### `getNetworkInfo`
79 |
80 | A function that returns a store containing information about a user's effect network speed using the `navigator.connection.effectiveType` API.
81 |
82 | This is the only value that can update and as such it returns a `readable` store instead of a static value. The store has the following contents:
83 |
84 | ```ts
85 | getNetworkInfo() = readable<{
86 | supported: Boolean,
87 | effectiveType:? Number
88 | }>;
89 | ```
90 |
91 | If `supported` is `false` then the `effectiveType` property will not be present.
92 |
93 | ```svelte
94 |
124 |
125 |
126 | ```
127 |
128 | ### `getMemoryInfo`
129 |
130 | A simple function that returns information about a user's deviceMemory using the `navigator.deviceMemory` and `performance.memory` APIs.
131 |
132 | This value is static and will never change.
133 |
134 | ```ts
135 | getMemoryInfo() = {
136 | supported: Boolean,
137 | deviceMemory:? Number,
138 | totalJSHeapSize:? Number,
139 | usedJSHeapSize:? Number,
140 | jsHeapSizeLimit:? Number,
141 | }
142 | ```
143 |
144 | If `supported` is `false` then the `deviceMemory`, `totalJSHeapSize`, `usedJSHeapSize`, `jsHeapSizeLimit` properties will not be present.
145 |
146 | ```svelte
147 |
152 |
153 | {#if supported && deviceMemory > 4}
154 |
155 | {:else}
156 |
157 | {/if}
158 | ```
159 |
160 | ### `getSaveDataInfo`
161 |
162 | A simple function that returns a user's current Save Data status
163 |
164 | ```ts
165 | getSaveDataInfo() = {
166 | supported: Boolean,
167 | saveData:? Boolean,
168 | };
169 | ```
170 |
171 | If `supported` is `false` then the `saveData`property will not be present.
172 |
173 | ```svelte
174 |
179 |
180 | {#if supported && !saveData}
181 |
182 | {:else}
183 |
184 | {/if}
185 | ```
186 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | [
4 | '@babel/preset-env',
5 | {
6 | targets: {
7 | node: 'current',
8 | },
9 | },
10 | ],
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | export { getNetworkInfo } from './src/network.js';
2 | export { getMemoryInfo } from './src/memory.js';
3 | export { getSaveDataInfo } from './src/savedata.js';
4 | export { getCpuInfo } from './src/cpu.js';
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-adaptive-sensors",
3 | "version": "0.1.2",
4 | "description": "Sensors for Svelte to help deliver adaptive behaviour based on a user's device and network.",
5 | "main": "index.js",
6 | "repository": "https://github.com/pngwn/svelte-adaptive-sensors",
7 | "scripts": {
8 | "test": "jest --verbose"
9 | },
10 | "keywords": [
11 | "svelte",
12 | "sensors",
13 | "loading",
14 | "performance",
15 | "store"
16 | ],
17 | "author": "pngwn ",
18 | "license": "MIT",
19 | "devDependencies": {
20 | "@babel/core": "^7.7.2",
21 | "@babel/preset-env": "^7.7.1",
22 | "babel-jest": "^24.9.0",
23 | "eslint": "^6.6.0",
24 | "eslint-plugin-import": "^2.18.2",
25 | "jest": "^24.9.0",
26 | "prettier": "^1.19.1",
27 | "prettier-eslint": "^9.0.0",
28 | "rollup": "^1.26.5",
29 | "svelte-test": "^0.4.0"
30 | },
31 | "dependencies": {},
32 | "peerDependencies": {
33 | "svelte": "^3.14.0"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/cpu.js:
--------------------------------------------------------------------------------
1 | export const getCpuInfo = () => {
2 | if (navigator !== undefined && 'hardwareConcurrency' in navigator) {
3 | return { processors: navigator.hardwareConcurrency, supported: true };
4 | } else {
5 | return { supported: false };
6 | }
7 | };
8 |
--------------------------------------------------------------------------------
/src/memory.js:
--------------------------------------------------------------------------------
1 | export const getMemoryInfo = () => {
2 | if (navigator !== undefined && 'deviceMemory' in navigator) {
3 | const performance_memory =
4 | 'memory' in performance ? performance.memory : null;
5 |
6 | return {
7 | deviceMemory: navigator.deviceMemory,
8 | totalJSHeapSize: performance_memory
9 | ? performance_memory.totalJSHeapSize
10 | : null,
11 | usedJSHeapSize: performance_memory
12 | ? performance_memory.usedJSHeapSize
13 | : null,
14 | jsHeapSizeLimit: performance_memory
15 | ? performance_memory.jsHeapSizeLimit
16 | : null,
17 | supported: true,
18 | };
19 | } else {
20 | return { supported: false };
21 | }
22 | };
23 |
--------------------------------------------------------------------------------
/src/network.js:
--------------------------------------------------------------------------------
1 | import { readable } from 'svelte/store';
2 |
3 | export const getNetworkInfo = () =>
4 | readable({}, set => {
5 | const update_network_status = () => {
6 | set({
7 | effectiveType: navigator.connection.effectiveType,
8 | supported: true,
9 | });
10 | };
11 |
12 | if (
13 | navigator !== undefined &&
14 | 'connection' in navigator &&
15 | 'effectiveType' in navigator.connection
16 | ) {
17 | set({
18 | effectiveType: navigator.connection.effectiveType,
19 | supported: true,
20 | });
21 | navigator.connection.addEventListener('change', update_network_status);
22 | } else {
23 | set({ supported: false });
24 | }
25 |
26 | return () => {
27 | if (
28 | navigator !== undefined &&
29 | 'connection' in navigator &&
30 | 'effectiveType' in navigator.connection
31 | ) {
32 | navigator.connection.removeEventListener(
33 | 'change',
34 | update_network_status
35 | );
36 | }
37 | };
38 | });
39 |
--------------------------------------------------------------------------------
/src/savedata.js:
--------------------------------------------------------------------------------
1 | export const getSaveDataInfo = () => {
2 | if (
3 | navigator !== undefined &&
4 | 'connection' in navigator &&
5 | 'saveData' in navigator.connection
6 | ) {
7 | return {
8 | supported: true,
9 | saveData: navigator.connection.saveData === true,
10 | };
11 | } else {
12 | return { supported: false };
13 | }
14 | };
15 |
--------------------------------------------------------------------------------
/test/cpu.spec.js:
--------------------------------------------------------------------------------
1 | import { getCpuInfo } from '../';
2 |
3 | test('should return the hardwareConcurrency value', () => {
4 | const result = getCpuInfo();
5 | expect(result.processors).toBe(window.navigator.hardwareConcurrency);
6 | });
7 |
8 | test('should return 17 if we have 17', () => {
9 | Object.defineProperty(window.navigator, 'hardwareConcurrency', {
10 | value: 17,
11 | configurable: true,
12 | writable: true,
13 | });
14 |
15 | const result = getCpuInfo();
16 | expect(result.processors).toEqual(17);
17 | });
18 |
19 | test('should return 1 if we have 1', () => {
20 | Object.defineProperty(window.navigator, 'hardwareConcurrency', {
21 | value: 1,
22 | configurable: true,
23 | writable: true,
24 | });
25 |
26 | const result = getCpuInfo();
27 | expect(result.processors).toEqual(1);
28 | });
29 |
--------------------------------------------------------------------------------
/test/memory.spec.js:
--------------------------------------------------------------------------------
1 | import { getMemoryInfo } from '../';
2 |
3 | test(`supported should be false if it isn't supported`, () => {
4 | const result = getMemoryInfo();
5 |
6 | expect(result.supported).toBe(false);
7 | });
8 |
9 | test(`it should work`, () => {
10 | const fake_memory = {
11 | deviceMemory: 1,
12 | totalJSHeapSize: 7,
13 | usedJSHeapSize: 100,
14 | jsHeapSizeLimit: 900,
15 | };
16 |
17 | navigator.deviceMemory = fake_memory.deviceMemory;
18 | performance.memory = {
19 | totalJSHeapSize: fake_memory.totalJSHeapSize,
20 | usedJSHeapSize: fake_memory.usedJSHeapSize,
21 | jsHeapSizeLimit: fake_memory.jsHeapSizeLimit,
22 | };
23 |
24 | const result = getMemoryInfo();
25 |
26 | expect(result).toEqual({ ...fake_memory, supported: true });
27 | });
28 |
29 | test(`it should work with anotehr ste of random values`, () => {
30 | const fake_memory = {
31 | deviceMemory: 4,
32 | totalJSHeapSize: 700,
33 | usedJSHeapSize: 101230,
34 | jsHeapSizeLimit: 91103230,
35 | };
36 |
37 | navigator.deviceMemory = fake_memory.deviceMemory;
38 | performance.memory = {
39 | totalJSHeapSize: fake_memory.totalJSHeapSize,
40 | usedJSHeapSize: fake_memory.usedJSHeapSize,
41 | jsHeapSizeLimit: fake_memory.jsHeapSizeLimit,
42 | };
43 |
44 | const result = getMemoryInfo();
45 |
46 | expect(result).toEqual({ ...fake_memory, supported: true });
47 | });
48 |
--------------------------------------------------------------------------------
/test/network.spec.js:
--------------------------------------------------------------------------------
1 | import { getNetworkInfo } from '../';
2 | import { get } from 'svelte/store';
3 |
4 | test('should return the current network type, add and remeove listeners', () => {
5 | // ask me no questions
6 | let cb;
7 | const add = jest.fn().mockImplementation((e, f) => (cb = f));
8 |
9 | const remove = jest.fn();
10 |
11 | navigator.connection = {
12 | effectiveType: '4g',
13 | addEventListener: add,
14 | removeEventListener: remove,
15 | };
16 |
17 | const result = get(getNetworkInfo());
18 |
19 | // `get` subscribes and unsubscribes so listeners should have been added and removed after this
20 |
21 | expect(add).toHaveBeenCalledWith('change', cb);
22 | expect(remove).toHaveBeenCalledWith('change', cb);
23 | expect(result.effectiveType).toEqual('4g');
24 | });
25 |
26 | test('when a change event is fired the value should update', () => {
27 | const add = jest.fn().mockImplementation((e, fn) => {
28 | events[e] = fn;
29 | });
30 | const remove = jest.fn();
31 | const events = {};
32 |
33 | navigator.connection = {
34 | effectiveType: '4g',
35 | addEventListener: add,
36 | removeEventListener: remove,
37 | };
38 |
39 | const store = getNetworkInfo();
40 | let v;
41 | const unsub = store.subscribe(_v => (v = _v));
42 |
43 | expect(v.effectiveType).toEqual('4g');
44 | navigator.connection.effectiveType = '3g';
45 | events.change();
46 |
47 | expect(add).toHaveBeenCalled();
48 | expect(v.effectiveType).toEqual('3g');
49 |
50 | unsub();
51 |
52 | expect(remove).toHaveBeenCalled();
53 | });
54 |
--------------------------------------------------------------------------------
/test/savedata.spec.js:
--------------------------------------------------------------------------------
1 | import { getSaveDataInfo } from '../';
2 |
3 | test('x.supported should be false when not supported', () => {
4 | const result = getSaveDataInfo();
5 | expect(result.supported).toBe(false);
6 | });
7 |
8 | test('if saveData is true then guess what', () => {
9 | navigator.connection = {
10 | saveData: true,
11 | };
12 |
13 | const result = getSaveDataInfo();
14 | expect(result.saveData).toEqual(true);
15 | expect(result.saveData).toEqual(navigator.connection.saveData);
16 | });
17 |
18 | test('if saveData is false then guess what', () => {
19 | navigator.connection = {
20 | saveData: false,
21 | };
22 |
23 | const result = getSaveDataInfo();
24 | expect(result.saveData).toEqual(false);
25 | expect(result.saveData).toEqual(navigator.connection.saveData);
26 | });
27 |
--------------------------------------------------------------------------------