Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

async-preloader

Package Overview
Dependencies
Maintainers
1
Versions
48
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

async-preloader - npm Package Compare versions

Comparing version 7.0.0 to 8.0.0

.nyc_output/lib/index.js

22

CHANGELOG.md

@@ -5,2 +5,24 @@ # Changelog

# [8.0.0](https://github.com/dmnsgn/async-preloader/compare/v7.0.0...v8.0.0) (2022-10-25)
### Bug Fixes
* check for unsupported features in nodes ([5ad3ae6](https://github.com/dmnsgn/async-preloader/commit/5ad3ae6e2c7e613f4c2be6d6d6a6830ec92801b2))
* reject with error event in loadAudio/Video ([5735f9a](https://github.com/dmnsgn/async-preloader/commit/5735f9a6bf040308bbc436a828e5f39d1aa30fa0))
### Features
* add noDecode ([7394e0d](https://github.com/dmnsgn/async-preloader/commit/7394e0d200a546d1c480866c3d8d5a711d5d1d2a))
* allow string for loadItem(s) ([7025c7d](https://github.com/dmnsgn/async-preloader/commit/7025c7d95f885b01c1121e1e8de877b61aa8ca81))
* use HTMLImageElement.decode() ([2388d40](https://github.com/dmnsgn/async-preloader/commit/2388d40d5fd134efab2ea01a67acb1574d91be64)), closes [#89](https://github.com/dmnsgn/async-preloader/issues/89)
### BREAKING CHANGES
* loadImage now decodes the image by default
# [7.0.0](https://github.com/dmnsgn/async-preloader/compare/v6.1.2...v7.0.0) (2022-06-13)

@@ -7,0 +29,0 @@

62

lib/index.js
import FontFaceObserver from "fontfaceobserver-es";
import { LoaderKey, } from "./types.js";
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) === true;
const isSafari = /^((?!chrome|android).)*safari/i.test(globalThis.navigator?.userAgent) ===
true;
/**

@@ -37,3 +38,3 @@ * AsyncPreloader: assets preloader using ES2017 async/await and fetch.

/**
* Default loader to use if no loader key is specified in the [[LoadItem]] or if the extension doesn't match any of the [[AsyncPreloader.loaders]] extensions
* Default loader to use if no loader key is specified in the {@link LoadItem} or if the extension doesn't match any of the {@link AsyncPreloader.loaders} extensions
*/

@@ -45,3 +46,3 @@ this.defaultLoader = LoaderKey.Text;

*
* @param {LoadItem[]} items Items to load
* @param {(LoadItem[] | string[])} items Items to load
* @returns {Promise<LoadedValue[]>} Resolve when all items are loaded, reject for any error

@@ -55,6 +56,8 @@ */

*
* @param {LoadItem} item Item to load
* @param {(LoadItem | string)} item Item to load
* @returns {Promise<LoadedValue>} Resolve when item is loaded, reject for any error
*/
this.loadItem = async (item) => {
if (typeof item === "string")
item = { src: item };
const extension = AsyncPreloader.getFileExtension(item.src || "");

@@ -140,21 +143,27 @@ const loaderKey = item.loader || AsyncPreloader.getLoaderKey(extension);

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLImageElement with a blob as srcObject or src.
* @returns {Promise<LoadedValue>} Fulfilled value with a decoded HTMLImageElement instance of or a parsed Response according to the "body" option. Defaults to a decoded HTMLImageElement.
*/
this.loadImage = async (item) => {
const response = await AsyncPreloader.fetchItem(item);
const data = await response[item.body || this.defaultBodyMethod]();
if (item.body)
return data;
const image = new Image();
return await new Promise((resolve, reject) => {
image.addEventListener("load", function load() {
image.removeEventListener("load", load);
resolve(image);
if (item.body) {
const response = await AsyncPreloader.fetchItem(item);
const data = await response[item.body]();
if (item.body !== "blob")
return data;
return await new Promise((resolve, reject) => {
image.addEventListener("load", function load() {
image.removeEventListener("load", load);
resolve(image);
});
image.addEventListener("error", function error(event) {
image.removeEventListener("error", error);
reject(event);
});
image.src = URL.createObjectURL(data);
});
image.addEventListener("error", function error() {
image.removeEventListener("error", error);
reject(image);
});
image.src = URL.createObjectURL(data);
});
}
image.src = item.src;
if (!item.noDecode)
await image.decode();
return image;
};

@@ -168,3 +177,3 @@ /**

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as src.
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as srcObject or src.
*/

@@ -182,5 +191,5 @@ this.loadVideo = async (item) => {

});
video.addEventListener("error", function error() {
video.addEventListener("error", function error(event) {
video.removeEventListener("error", error);
reject(video);
reject(event);
});

@@ -220,5 +229,5 @@ try {

});
audio.addEventListener("error", function error() {
audio.addEventListener("error", function error(event) {
audio.removeEventListener("error", error);
reject(audio);
reject(event);
});

@@ -253,2 +262,5 @@ try {

}
if (!AsyncPreloader.domParser) {
throw new Error("DomParser is not supported.");
}
const response = await AsyncPreloader.fetchItem(item);

@@ -362,3 +374,3 @@ const data = await response.text();

*
* Allows the omission of the loader key in a [[LoadItem.loader]] for some generic extensions
* Allows the omission of the loader key in a {@link LoadItem.loader} for some generic extensions
*/

@@ -365,0 +377,0 @@ AsyncPreloader.loaders = new Map()

/**
* Keys used for the [[AsyncPreloader.loaders]]
* Keys used for the {@link AsyncPreloader.loaders}
*/

@@ -4,0 +4,0 @@ export var LoaderKey;

{
"name": "async-preloader",
"version": "7.0.0",
"description": "Assets preloader using ES2017 async/await and fetch.",
"version": "8.0.0",
"description": "Assets preloader using async/await and fetch for usage both in the browser and Node.js.",
"keywords": [

@@ -11,3 +11,4 @@ "preloader",

"typescript",
"ES2017"
"ES2017",
"fetch"
],

@@ -34,4 +35,5 @@ "homepage": "https://github.com/dmnsgn/async-preloader",

"scripts": {
"coverage": "nyc report --reporter=text --reporter=html",
"prejest": "npx snowdev install",
"jest": "NODE_OPTIONS=--experimental-vm-modules npx jest test/ --watch --coverage --no-cache",
"jest": "NODE_OPTIONS=--experimental-vm-modules npx jest test/ --watch --no-cache",
"pretest": "npx snowdev install && npx snowdev build --no-docs --no-lint",

@@ -41,13 +43,4 @@ "test": "NODE_OPTIONS=--experimental-vm-modules npx jest test/ --runInBand"

"jest": {
"collectCoverageFrom": [
"src/**/*.ts"
],
"coverageDirectory": "coverage",
"globalSetup": "jest-environment-puppeteer/setup",
"globalTeardown": "jest-environment-puppeteer/teardown",
"globals": {
"ts-jest": {
"useESM": true
}
},
"globalTeardown": "<rootDir>/test/global-teardown.js",
"moduleNameMapper": {

@@ -58,12 +51,13 @@ "^(\\.{1,2}/.*)\\.js$": "$1"

"resolver": "jest-ts-webcompat-resolver",
"setupFiles": [
"<rootDir>/test/setupFile.js"
],
"setupFilesAfterEnv": [
"expect-puppeteer"
],
"testEnvironment": "jsdom",
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(ts|js)x?$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
"^.+\\.(t|j)s$": [
"ts-jest",
{
"useESM": true
}
]
}

@@ -77,20 +71,20 @@ },

"@types/css-font-loading-module": "^0.0.7",
"@types/expect-puppeteer": "^5.0.0",
"@types/jest": "^28.1.1",
"@types/expect-puppeteer": "^5.0.1",
"@types/jest": "^29.2.0",
"@types/jest-environment-puppeteer": "^5.0.2",
"@types/node": "^17.0.42",
"@types/puppeteer": "^5.4.6",
"@xmldom/xmldom": "^0.8.2",
"es-module-shims": "^1.5.6",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jest-puppeteer": "^6.1.0",
"@types/node": "^18.11.3",
"@types/puppeteer": "^5.4.7",
"@xmldom/xmldom": "^0.8.3",
"es-module-shims": "^1.6.2",
"jest": "^29.2.1",
"jest-puppeteer": "^6.1.1",
"jest-ts-webcompat-resolver": "^1.0.0",
"jsdom": "^19.0.0",
"mime-types": "^2.1.35",
"node-fetch": "^3.2.6",
"puppeteer": "^14.3.0",
"snowdev": "^1.11.0",
"ts-jest": "^28.0.4",
"typescript": "^4.7.3"
"nyc": "^15.1.0",
"portfinder": "^1.0.32",
"puppeteer": "^19.0.0",
"puppeteer-to-istanbul": "^1.4.0",
"snowdev": "^1.13.0",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4"
},

@@ -97,0 +91,0 @@ "engines": {

@@ -14,3 +14,3 @@ # async-preloader

Assets preloader using ES2017 async/await and fetch.
Assets preloader using async/await and fetch for usage both in the browser and Node.js.

@@ -34,3 +34,3 @@ [![paypal](https://img.shields.io/badge/donate-paypal-informational?logo=paypal)](https://paypal.me/dmnsgn)

This section covers the basic usage of `AsyncPreloader`. For more informations about async/await, see [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/primers/async-functions). Usage in Node.js environment is limited to its capacity to handle `fetch` requests. Polyfills like [`node-fetch`](https://www.npmjs.com/package/node-fetch) and [`xmldom`](https://www.npmjs.com/package/xmldom) might come handy.
This section covers the basic usage of `AsyncPreloader`. For more informations about async/await, see [Async functions - making promises friendly](https://developers.google.com/web/fundamentals/primers/async-functions). Usage in Node.js environment is limited to its capacity to handle `fetch` requests and DOM APIs. Polyfills like [`undici`](https://www.npmjs.com/package/undici/)/[`node-fetch`](https://www.npmjs.com/package/node-fetch) (for Node.js below 18) and [`xmldom`](https://www.npmjs.com/package/xmldom) might come handy .

@@ -74,3 +74,3 @@ ### Preload items and retrieve them

Note: Font loader is will try to detect the font in the page using [FontFaceObserver](https://github.com/dmnsgn/fontfaceobserver) when no src is specified.
Note: Font loader will try to detect the font in the page using [FontFaceObserver](https://github.com/dmnsgn/fontfaceobserver) when no src is specified.

@@ -104,3 +104,3 @@ ### Load items from a manifest file

### Load a single item by using the [loaders](https://github.com/dmnsgn/async-preloader/blob/master/src/index.ts#L40) directly
### Load a single item by using the [loaders](https://github.com/dmnsgn/async-preloader/blob/main/src/index.ts#L60) directly

@@ -125,2 +125,17 @@ ```javascript

### Load a single item by using a string directly
```javascript
import AsyncPreloader from "async-preloader";
try {
// Pass a string
//
// Returns a Promise with the LoadedValue
const pItem = await AsyncPreloader.loadItem("assets/json.json");
} catch (error) {
console.error(error);
}
```
### Get an `ArrayBuffer` instead of the default `Blob`

@@ -198,8 +213,4 @@

---
Note: the example above uses the async functions (which is the core of this module). You'll need to transpile it if you are targeting older browsers (namely IE11). See support [here](https://caniuse.com/#feat=async-functions).
## License
MIT © [Damien Seguin](https://github.com/dmnsgn)

@@ -14,3 +14,4 @@ import FontFaceObserver from "fontfaceobserver-es";

const isSafari =
/^((?!chrome|android).)*safari/i.test(navigator.userAgent) === true;
/^((?!chrome|android).)*safari/i.test(globalThis.navigator?.userAgent) ===
true;

@@ -51,3 +52,3 @@ /**

/**
* Default loader to use if no loader key is specified in the [[LoadItem]] or if the extension doesn't match any of the [[AsyncPreloader.loaders]] extensions
* Default loader to use if no loader key is specified in the {@link LoadItem} or if the extension doesn't match any of the {@link AsyncPreloader.loaders} extensions
*/

@@ -59,3 +60,3 @@ public defaultLoader: LoaderKey = LoaderKey.Text;

*
* Allows the omission of the loader key in a [[LoadItem.loader]] for some generic extensions
* Allows the omission of the loader key in a {@link LoadItem.loader} for some generic extensions
*/

@@ -91,6 +92,8 @@ private static loaders: Map<LoaderKey, LoaderValue> = new Map()

*
* @param {LoadItem[]} items Items to load
* @param {(LoadItem[] | string[])} items Items to load
* @returns {Promise<LoadedValue[]>} Resolve when all items are loaded, reject for any error
*/
public loadItems = async (items: LoadItem[]): Promise<LoadedValue[]> => {
public loadItems = async (
items: LoadItem[] | string[]
): Promise<LoadedValue[]> => {
return await Promise.all(items.map(this.loadItem));

@@ -102,6 +105,8 @@ };

*
* @param {LoadItem} item Item to load
* @param {(LoadItem | string)} item Item to load
* @returns {Promise<LoadedValue>} Resolve when item is loaded, reject for any error
*/
public loadItem = async (item: LoadItem): Promise<LoadedValue> => {
public loadItem = async (item: LoadItem | string): Promise<LoadedValue> => {
if (typeof item === "string") item = { src: item };
const extension: string = AsyncPreloader.getFileExtension(

@@ -204,25 +209,31 @@ (item.src as string) || ""

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLImageElement with a blob as srcObject or src.
* @returns {Promise<LoadedValue>} Fulfilled value with a decoded HTMLImageElement instance of or a parsed Response according to the "body" option. Defaults to a decoded HTMLImageElement.
*/
public loadImage = async (item: LoadItem): Promise<LoadedValue> => {
const response: Response = await AsyncPreloader.fetchItem(item);
const data: LoadedValue = await response[
item.body || this.defaultBodyMethod
]();
const image = new Image();
if (item.body) return data;
if (item.body) {
const response: Response = await AsyncPreloader.fetchItem(item);
const data: LoadedValue = await response[item.body]();
const image = new Image();
if (item.body !== "blob") return data;
return await new Promise<HTMLImageElement>((resolve, reject) => {
image.addEventListener("load", function load() {
image.removeEventListener("load", load);
resolve(image);
return await new Promise<HTMLImageElement>((resolve, reject) => {
image.addEventListener("load", function load() {
image.removeEventListener("load", load);
resolve(image);
});
image.addEventListener("error", function error(event) {
image.removeEventListener("error", error);
reject(event);
});
image.src = URL.createObjectURL(data as Blob);
});
image.addEventListener("error", function error() {
image.removeEventListener("error", error);
reject(image);
});
image.src = URL.createObjectURL(data as Blob);
});
}
image.src = item.src as string;
if (!item.noDecode) await image.decode();
return image;
};

@@ -237,3 +248,3 @@

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as src.
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as srcObject or src.
*/

@@ -255,5 +266,5 @@ public loadVideo = async (item: LoadItem): Promise<LoadedValue> => {

});
video.addEventListener("error", function error() {
video.addEventListener("error", function error(event) {
video.removeEventListener("error", error);
reject(video);
reject(event);
});

@@ -298,5 +309,5 @@

});
audio.addEventListener("error", function error() {
audio.addEventListener("error", function error(event) {
audio.removeEventListener("error", error);
reject(audio);
reject(event);
});

@@ -335,2 +346,6 @@

if (!AsyncPreloader.domParser) {
throw new Error("DomParser is not supported.");
}
const response: Response = await AsyncPreloader.fetchItem(item);

@@ -337,0 +352,0 @@ const data: string = await response.text();

@@ -7,3 +7,3 @@ /**

/**
* Types that can be returned by all the [[BodyMethod]]
* Types that can be returned by all the {@link BodyMethod}
*/

@@ -13,3 +13,3 @@ export type BodyResolveValue = ArrayBuffer | Blob | FormData | JSON | string;

/**
* Types that can be returned by the Xml loader. See the [[LoadItem.mimeType]].
* Types that can be returned by the Xml loader. See the {@link LoadItem.mimeType}.
*/

@@ -39,10 +39,10 @@ export type LoadedXMLValue = Document | XMLDocument;

*
* Used to retrieve the [[LoadedValue]] using `AsyncPreloader.items.get(id)`
* Used to retrieve the {@link LoadedValue} using `AsyncPreloader.items.get(id)`
*/
id?: unknown;
/**
* Optional [[LoaderKey]].
* Optional {@link LoaderKey}.
*
* If none specified, the loader is inferred from the file extension.
* Default to `Text` if the extension doesn't match any of the extensions specified in [[AsyncPreloader.loaders]].
* Default to `Text` if the extension doesn't match any of the extensions specified in {@link AsyncPreloader.loaders}.
*

@@ -61,5 +61,5 @@ * Note: It needs to be specified for Font and Audio (webm, ogg).

/**
* Optional [[BodyMethod]] used to handle the Response.
* Optional{@link BodyMethod} used to handle the Response.
*
* Default to `blob` for Image, Video and Audio. See [[AsyncPreloader.defaultBodyMethod]].
* Default to `blob` for Image, Video and Audio. See {@link AsyncPreloader.defaultBodyMethod}.
*/

@@ -73,6 +73,12 @@ body?: BodyMethod;

mimeType?: DOMParserSupportedType;
/**
* Optional disable [image decoding](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode).
*
* Note: Only used for loadImage.
*/
noDecode?: boolean;
}
/**
* Keys used for the [[AsyncPreloader.loaders]]
* Keys used for the {@link AsyncPreloader.loaders}
*/

@@ -116,7 +122,7 @@ export enum LoaderKey {

/**
* Values used for the [[AsyncPreloader.loaders]]
* Values used for the {@link AsyncPreloader.loaders}
*/
export interface LoaderValue {
/**
* [[LoadItem]] with no loader key specified will use the following array to find which loader should be used.
* {@link LoadItem} with no loader key specified will use the following array to find which loader should be used.
*/

@@ -123,0 +129,0 @@ extensions: string[];

@@ -33,3 +33,3 @@ import { BodyMethod, LoadItem, LoadedValue, LoadedXMLValue, LoaderKey } from "./types.js";

/**
* Default loader to use if no loader key is specified in the [[LoadItem]] or if the extension doesn't match any of the [[AsyncPreloader.loaders]] extensions
* Default loader to use if no loader key is specified in the {@link LoadItem} or if the extension doesn't match any of the {@link AsyncPreloader.loaders} extensions
*/

@@ -40,3 +40,3 @@ defaultLoader: LoaderKey;

*
* Allows the omission of the loader key in a [[LoadItem.loader]] for some generic extensions
* Allows the omission of the loader key in a {@link LoadItem.loader} for some generic extensions
*/

@@ -51,13 +51,13 @@ private static loaders;

*
* @param {LoadItem[]} items Items to load
* @param {(LoadItem[] | string[])} items Items to load
* @returns {Promise<LoadedValue[]>} Resolve when all items are loaded, reject for any error
*/
loadItems: (items: LoadItem[]) => Promise<LoadedValue[]>;
loadItems: (items: LoadItem[] | string[]) => Promise<LoadedValue[]>;
/**
* Load a single item
*
* @param {LoadItem} item Item to load
* @param {(LoadItem | string)} item Item to load
* @returns {Promise<LoadedValue>} Resolve when item is loaded, reject for any error
*/
loadItem: (item: LoadItem) => Promise<LoadedValue>;
loadItem: (item: LoadItem | string) => Promise<LoadedValue>;
/**

@@ -113,3 +113,3 @@ * Load a manifest of items

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLImageElement with a blob as srcObject or src.
* @returns {Promise<LoadedValue>} Fulfilled value with a decoded HTMLImageElement instance of or a parsed Response according to the "body" option. Defaults to a decoded HTMLImageElement.
*/

@@ -124,3 +124,3 @@ loadImage: (item: LoadItem) => Promise<LoadedValue>;

* @param {LoadItem} item Item to load
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as src.
* @returns {Promise<LoadedValue>} Fulfilled value of parsed Response according to the "body" option. Defaults to an HTMLVideoElement with a blob as srcObject or src.
*/

@@ -127,0 +127,0 @@ loadVideo: (item: LoadItem) => Promise<LoadedValue>;

@@ -6,7 +6,7 @@ /**

/**
* Types that can be returned by all the [[BodyMethod]]
* Types that can be returned by all the {@link BodyMethod}
*/
export declare type BodyResolveValue = ArrayBuffer | Blob | FormData | JSON | string;
/**
* Types that can be returned by the Xml loader. See the [[LoadItem.mimeType]].
* Types that can be returned by the Xml loader. See the {@link LoadItem.mimeType}.
*/

@@ -29,10 +29,10 @@ export declare type LoadedXMLValue = Document | XMLDocument;

*
* Used to retrieve the [[LoadedValue]] using `AsyncPreloader.items.get(id)`
* Used to retrieve the {@link LoadedValue} using `AsyncPreloader.items.get(id)`
*/
id?: unknown;
/**
* Optional [[LoaderKey]].
* Optional {@link LoaderKey}.
*
* If none specified, the loader is inferred from the file extension.
* Default to `Text` if the extension doesn't match any of the extensions specified in [[AsyncPreloader.loaders]].
* Default to `Text` if the extension doesn't match any of the extensions specified in {@link AsyncPreloader.loaders}.
*

@@ -51,5 +51,5 @@ * Note: It needs to be specified for Font and Audio (webm, ogg).

/**
* Optional [[BodyMethod]] used to handle the Response.
* Optional{@link BodyMethod} used to handle the Response.
*
* Default to `blob` for Image, Video and Audio. See [[AsyncPreloader.defaultBodyMethod]].
* Default to `blob` for Image, Video and Audio. See {@link AsyncPreloader.defaultBodyMethod}.
*/

@@ -63,5 +63,11 @@ body?: BodyMethod;

mimeType?: DOMParserSupportedType;
/**
* Optional disable [image decoding](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/decode).
*
* Note: Only used for loadImage.
*/
noDecode?: boolean;
}
/**
* Keys used for the [[AsyncPreloader.loaders]]
* Keys used for the {@link AsyncPreloader.loaders}
*/

@@ -102,7 +108,7 @@ export declare enum LoaderKey {

/**
* Values used for the [[AsyncPreloader.loaders]]
* Values used for the {@link AsyncPreloader.loaders}
*/
export interface LoaderValue {
/**
* [[LoadItem]] with no loader key specified will use the following array to find which loader should be used.
* {@link LoadItem} with no loader key specified will use the following array to find which loader should be used.
*/

@@ -109,0 +115,0 @@ extensions: string[];

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc