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

@internetarchive/lazy-loader-service

Package Overview
Dependencies
Maintainers
13
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@internetarchive/lazy-loader-service - npm Package Compare versions

Comparing version 0.1.0 to 0.2.0-alpha.1

.github/workflows/ci.yml

4

dist/index.d.ts

@@ -1,3 +0,3 @@

export { LazyLoaderService } from './src/lazy-loader-service';
export { LazyLoaderServiceInterface } from './src/lazy-loader-service-interface';
export { LazyLoaderService, LazyLoaderServiceOptions, } from './src/lazy-loader-service';
export { LazyLoaderServiceInterface, LazyLoaderServiceEvents, } from './src/lazy-loader-service-interface';
export { BundleType } from './src/bundle-type';

@@ -1,3 +0,2 @@

export { LazyLoaderService } from './src/lazy-loader-service';
export { BundleType } from './src/bundle-type';
export { LazyLoaderService, } from './src/lazy-loader-service';
//# sourceMappingURL=index.js.map

@@ -1,4 +0,1 @@

export declare enum BundleType {
Module = "module",
NoModule = "nomodule"
}
export declare type BundleType = 'module' | 'nomodule';

@@ -1,6 +0,2 @@

export var BundleType;
(function (BundleType) {
BundleType["Module"] = "module";
BundleType["NoModule"] = "nomodule";
})(BundleType || (BundleType = {}));
export {};
//# sourceMappingURL=bundle-type.js.map
import { BundleType } from './bundle-type';
import { Unsubscribe } from 'nanoevents';
export interface LazyLoaderServiceEvents {
scriptLoadRetried: (src: string, retryCount: number) => void;
scriptLoadFailed: (src: string, error: string | Event) => void;
}
export interface LazyLoaderServiceInterface {
/**
* Bind to receive notifications about retry and failure events
*
* @template E
* @param {E} event
* @param {LazyLoaderServiceEvents[E]} callback
* @returns {Unsubscribe}
* @memberof LazyLoaderServiceInterface
*/
on<E extends keyof LazyLoaderServiceEvents>(event: E, callback: LazyLoaderServiceEvents[E]): Unsubscribe;
/**
* Load a javascript bundle (module and nomodule pair)

@@ -18,3 +33,3 @@ *

nomodule?: string;
}): Promise<Event | undefined>;
}): Promise<void>;
/**

@@ -35,7 +50,4 @@ * Load a script with a Promise

bundleType?: BundleType;
attributes?: {
key: string;
value: any;
}[];
}): Promise<Event>;
attributes?: Record<string, string>;
}): Promise<void>;
}

@@ -0,1 +1,2 @@

export {};
//# sourceMappingURL=lazy-loader-service-interface.js.map

@@ -1,11 +0,36 @@

import { BundleType } from './bundle-type';
import { LazyLoaderServiceInterface } from './lazy-loader-service-interface';
import { Unsubscribe } from 'nanoevents';
import type { BundleType } from './bundle-type';
import { LazyLoaderServiceEvents, LazyLoaderServiceInterface } from './lazy-loader-service-interface';
export interface LazyLoaderServiceOptions {
/**
* The HTMLElement in which we put the script tags, defaults to document.head
*/
container?: HTMLElement;
/**
* The number of retries we should attempt
*/
retryCount?: number;
/**
* The retry interval in seconds
*/
retryInterval?: number;
}
export declare class LazyLoaderService implements LazyLoaderServiceInterface {
private container;
constructor(container?: HTMLElement);
private retryCount;
private retryInterval;
private emitter;
/**
* LazyLoaderService constructor
*
* @param options LazyLoaderServiceOptions
*/
constructor(options?: LazyLoaderServiceOptions);
/** @inheritdoc */
on<E extends keyof LazyLoaderServiceEvents>(event: E, callback: LazyLoaderServiceEvents[E]): Unsubscribe;
/** @inheritdoc */
loadBundle(bundle: {
module?: string;
nomodule?: string;
}): Promise<Event | undefined>;
}): Promise<void>;
/** @inheritdoc */

@@ -15,7 +40,12 @@ loadScript(options: {

bundleType?: BundleType;
attributes?: {
key: string;
value: any;
}[];
}): Promise<Event>;
attributes?: Record<string, string>;
}): Promise<void>;
private doLoad;
/**
* Generate a script tag with all of the proper attributes
*
* @param options
* @returns
*/
private getScriptTag;
}

@@ -1,87 +0,129 @@

import { BundleType } from './bundle-type';
import { __awaiter } from "tslib";
import { createNanoEvents } from 'nanoevents';
import { promisedSleep } from './promised-sleep';
export class LazyLoaderService {
constructor(container = document.head) {
this.container = container;
/**
* LazyLoaderService constructor
*
* @param options LazyLoaderServiceOptions
*/
constructor(options) {
var _a, _b, _c;
// the emitter for consumers to listen for events like retrying a load
this.emitter = createNanoEvents();
this.container = (_a = options === null || options === void 0 ? void 0 : options.container) !== null && _a !== void 0 ? _a : document.head;
this.retryCount = (_b = options === null || options === void 0 ? void 0 : options.retryCount) !== null && _b !== void 0 ? _b : 2;
this.retryInterval = (_c = options === null || options === void 0 ? void 0 : options.retryInterval) !== null && _c !== void 0 ? _c : 1;
}
/** @inheritdoc */
on(event, callback) {
return this.emitter.on(event, callback);
}
/** @inheritdoc */
loadBundle(bundle) {
let modulePromise;
let nomodulePromise;
/* istanbul ignore else */
if (bundle.module) {
modulePromise = this.loadScript({
src: bundle.module,
bundleType: BundleType.Module,
});
}
/* istanbul ignore else */
if (bundle.nomodule) {
nomodulePromise = this.loadScript({
src: bundle.nomodule,
bundleType: BundleType.NoModule,
});
}
return Promise.race([modulePromise, nomodulePromise]);
return __awaiter(this, void 0, void 0, function* () {
let modulePromise;
let nomodulePromise;
/* istanbul ignore else */
if (bundle.module) {
modulePromise = this.loadScript({
src: bundle.module,
bundleType: 'module',
});
}
/* istanbul ignore else */
if (bundle.nomodule) {
nomodulePromise = this.loadScript({
src: bundle.nomodule,
bundleType: 'nomodule',
});
}
return Promise.race([modulePromise, nomodulePromise]);
});
}
/** @inheritdoc */
loadScript(options) {
return __awaiter(this, void 0, void 0, function* () {
return this.doLoad(options);
});
}
doLoad(options) {
var _a;
const scriptSelector = `script[src='${options.src}'][async]`;
let script = this.container.querySelector(scriptSelector);
if (!script) {
script = document.createElement('script');
script.setAttribute('src', options.src);
script.async = true;
const attributes = (_a = options.attributes) !== null && _a !== void 0 ? _a : [];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributes.forEach((element) => {
// eslint-disable-next-line no-unused-expressions
script.setAttribute(element.key, element.value);
});
switch (options.bundleType) {
case BundleType.Module:
script.setAttribute('type', options.bundleType);
break;
// cannot be tested because modern browsers ignore `nomodule`
/* istanbul ignore next */
case BundleType.NoModule:
script.setAttribute(options.bundleType, '');
break;
default:
break;
return __awaiter(this, void 0, void 0, function* () {
const retryCount = (_a = options.retryCount) !== null && _a !== void 0 ? _a : 0;
const scriptSelector = `script[src='${options.src}'][async][retryCount='${retryCount}']`;
let script = this.container.querySelector(scriptSelector);
if (!script) {
script = this.getScriptTag(options);
this.container.appendChild(script);
}
}
return new Promise((resolve, reject) => {
// if multiple requests get made for this script, just stack the onloads
// and onerrors and all the callbacks will be called in-order of being received
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalOnLoad = script.onload;
script.onload = (event) => {
if (originalOnLoad) {
originalOnLoad(event);
return new Promise((resolve, reject) => {
// script has already been loaded, just resolve
if (script.getAttribute('dynamicImportLoaded')) {
resolve();
return;
}
script.setAttribute('dynamicImportLoaded', 'true');
resolve(event);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalOnError = script.onerror;
script.onerror = (error) => {
if (originalOnError) {
originalOnError(error);
}
/* istanbul ignore else */
if (script.parentNode) {
script.parentNode.removeChild(script);
}
reject(error);
};
if (script.parentNode === null) {
this.container.appendChild(script);
}
else if (script.getAttribute('dynamicImportLoaded')) {
resolve();
}
const scriptBeingRetried = options.scriptBeingRetried;
// If multiple requests get made for this script, just stack the `onload`s
// and `onerror`s and all the callbacks will be called in-order of being received.
// If we are retrying the load, we use the `onload` / `onerror` from the script being retried
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalOnLoad = script.onload || (scriptBeingRetried === null || scriptBeingRetried === void 0 ? void 0 : scriptBeingRetried.onload);
script.onload = (event) => {
originalOnLoad === null || originalOnLoad === void 0 ? void 0 : originalOnLoad(event);
script.setAttribute('dynamicImportLoaded', 'true');
resolve();
};
const originalOnError = script.onerror || (scriptBeingRetried === null || scriptBeingRetried === void 0 ? void 0 : scriptBeingRetried.onerror);
script.onerror = (error) => __awaiter(this, void 0, void 0, function* () {
const hasBeenRetried = script.getAttribute('hasBeenRetried');
if (retryCount < this.retryCount && !hasBeenRetried) {
script.setAttribute('hasBeenRetried', 'true');
yield promisedSleep(this.retryInterval * 1000);
const newRetryCount = retryCount + 1;
this.emitter.emit('scriptLoadRetried', options.src, newRetryCount);
this.doLoad(Object.assign(Object.assign({}, options), { retryCount: newRetryCount, scriptBeingRetried: script }));
}
else {
this.emitter.emit('scriptLoadFailed', options.src, error);
originalOnError === null || originalOnError === void 0 ? void 0 : originalOnError(error);
reject(error);
}
});
});
});
}
/**
* Generate a script tag with all of the proper attributes
*
* @param options
* @returns
*/
getScriptTag(options) {
var _a, _b;
const fixedSrc = options.src.replace("'", '"');
const script = document.createElement('script');
const retryCount = (_a = options.retryCount) !== null && _a !== void 0 ? _a : 0;
script.setAttribute('src', fixedSrc);
script.setAttribute('retryCount', retryCount.toString());
script.async = true;
const attributes = (_b = options.attributes) !== null && _b !== void 0 ? _b : {};
Object.keys(attributes).forEach(key => {
script.setAttribute(key, attributes[key]);
});
switch (options.bundleType) {
case 'module':
script.setAttribute('type', options.bundleType);
break;
// cannot be tested because modern browsers ignore `nomodule`
/* istanbul ignore next */
case 'nomodule':
script.setAttribute(options.bundleType, '');
break;
default:
break;
}
return script;
}
}
//# sourceMappingURL=lazy-loader-service.js.map
import { __awaiter } from "tslib";
import { expect, fixture, html } from '@open-wc/testing';
import { LazyLoaderService } from '../src/lazy-loader-service';
import { BundleType } from '../src/bundle-type';
const testServiceUrl = '/base/dist/test/test-service.js';

@@ -16,3 +15,3 @@ describe('Lazy Loader Service', () => {

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadBundle({

@@ -29,3 +28,3 @@ module: testServiceUrl,

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadScript({ src: testServiceUrl });

@@ -35,17 +34,5 @@ const scripts = container.querySelectorAll('script');

}));
it('Removes the script tag if the load fails', () => __awaiter(void 0, void 0, void 0, function* () {
const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
try {
yield lazyLoader.loadScript({ src: './blahblahnotfound.js' });
}
catch (_a) {
// expected failure
}
const scripts = container.querySelectorAll('script');
expect(scripts.length).to.equal(0);
}));
it('Only loads scripts once if called multiple times', () => __awaiter(void 0, void 0, void 0, function* () {
const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadScript({ src: testServiceUrl });

@@ -59,3 +46,3 @@ yield lazyLoader.loadScript({ src: testServiceUrl });

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadScript({ src: testServiceUrl });

@@ -68,6 +55,6 @@ // eslint-disable-next-line @typescript-eslint/no-explicit-any

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadScript({
src: testServiceUrl,
attributes: [{ key: 'foo', value: 'bar' }],
attributes: { foo: 'bar' },
});

@@ -80,6 +67,6 @@ const script = container.querySelector('script');

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
yield lazyLoader.loadScript({
src: testServiceUrl,
bundleType: BundleType.Module,
bundleType: 'module',
});

@@ -94,3 +81,3 @@ const script = container.querySelector('script');

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
const count = 25;

@@ -119,3 +106,6 @@ const loads = new Array(count).fill(false);

const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({
container,
retryInterval: 0.1,
});
const count = 25;

@@ -145,4 +135,81 @@ const loadFailed = new Array(count).fill(false);

}));
// This is verifying that we emit an event on retry and failure.
// This allows the consumer to respond to these events with analytics handling
// anything else.
it('Emits an event when a retry occurs or fails', () => __awaiter(void 0, void 0, void 0, function* () {
const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService({
container,
retryCount: 1,
retryInterval: 0.01,
});
let testRetryCount = 0;
lazyLoader.on('scriptLoadRetried', (src, retryCount) => {
testRetryCount = retryCount;
});
let scriptLoadEventFired = false;
let failedSrc;
lazyLoader.on('scriptLoadFailed', (src) => {
scriptLoadEventFired = true;
failedSrc = src;
});
let retryFailed = false;
try {
yield lazyLoader.loadScript({ src: '/base/test/blahblah.js' });
}
catch (err) {
retryFailed = true;
}
expect(testRetryCount).to.equal(1);
expect(scriptLoadEventFired).to.be.true;
expect(failedSrc).to.equal('/base/test/blahblah.js');
expect(retryFailed).to.be.true;
}));
it('Retries the specified number of times', () => __awaiter(void 0, void 0, void 0, function* () {
const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService({
container,
retryCount: 5,
retryInterval: 0.01,
});
try {
yield lazyLoader.loadScript({ src: '/base/test/blahblah.js' });
}
catch (_a) { }
const scriptTags = container.querySelectorAll('script');
expect(scriptTags.length).to.equal(6);
}));
/**
* This is a special test that connects to a sidecar node server to test retrying a request.
*
* In `npm run test`, we run `test/test-server.js` while we're running our tests.
* `test-server.js` has a very specific purpose:
*
* The very first request will always return an HTTP 404, the second request returns a HTTP 200,
* then it shuts down.
*
* This lets us check that a failed request gets retried successfully.
*/
it('Can retry a reqest', () => __awaiter(void 0, void 0, void 0, function* () {
const serverUrl = 'http://localhost:5432/';
const container = (yield fixture(html ` <div></div> `));
const lazyLoader = new LazyLoaderService({
container,
retryCount: 1,
retryInterval: 0.01,
});
yield lazyLoader.loadScript({ src: serverUrl });
// verify we have two script tags with the expected url and a propery retryCount on each
const scriptTags = container.querySelectorAll('script');
scriptTags.forEach((tag, index) => {
expect(tag.src).to.equal(serverUrl);
expect(tag.getAttribute('retryCount'), `${index}`);
});
// verify the final load actually loaded the service
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = window.otherService.getResponse();
expect(result).to.equal('someotherresponse');
}));
});
});
//# sourceMappingURL=lazy-loader-service.test.js.map

@@ -1,3 +0,9 @@

export { LazyLoaderService } from './src/lazy-loader-service';
export { LazyLoaderServiceInterface } from './src/lazy-loader-service-interface';
export {
LazyLoaderService,
LazyLoaderServiceOptions,
} from './src/lazy-loader-service';
export {
LazyLoaderServiceInterface,
LazyLoaderServiceEvents,
} from './src/lazy-loader-service-interface';
export { BundleType } from './src/bundle-type';
{
"name": "@internetarchive/lazy-loader-service",
"version": "0.1.0",
"version": "0.2.0-alpha.1",
"description": "A small library to lazy load javascript with a Promise",

@@ -10,3 +10,3 @@ "license": "AGPL-3.0-only",

"scripts": {
"start": "concurrently --kill-others --names tsc,es-dev-server \"npm run tsc:watch\" \"es-dev-server --app-index demo/index.html --node-resolve --open --watch\"",
"start": "concurrently --kill-others --names tsc,wds \"npm run tsc:watch\" \"wds --app-index demo/index.html --node-resolve --open --watch\"",
"docs": "typedoc",

@@ -20,6 +20,5 @@ "tsc:watch": "tsc --watch",

"format": "npm run format:eslint && npm run format:prettier",
"test": "npm run lint && tsc && karma start --coverage",
"test": "npm run lint && tsc && concurrently \"node ./test/retry-server.js\" \"karma start --coverage\"",
"test:watch": "concurrently --kill-others --names tsc,karma \"npm run tsc:watch\" \"karma start --auto-watch=true --single-run=false\""
},
"dependencies": {},
"devDependencies": {

@@ -32,5 +31,5 @@ "@open-wc/eslint-config": "^2.0.0",

"@typescript-eslint/parser": "^2.20.0",
"@web/dev-server": "^0.1.28",
"concurrently": "^5.1.0",
"deepmerge": "^3.2.0",
"es-dev-server": "^1.23.0",
"eslint": "^6.1.0",

@@ -42,5 +41,5 @@ "eslint-config-prettier": "^6.11.0",

"prettier": "^2.0.4",
"tslib": "^1.11.0",
"typedoc": "^0.17.8",
"typescript": "~3.8.2"
"tslib": "^2.3.1",
"typedoc": "^0.22.10",
"typescript": "^4.5.2"
},

@@ -68,3 +67,6 @@ "eslintConfig": {

]
},
"dependencies": {
"nanoevents": "^6.0.2"
}
}

@@ -1,4 +0,1 @@

export enum BundleType {
Module = 'module',
NoModule = 'nomodule',
}
export type BundleType = 'module' | 'nomodule';
import { BundleType } from './bundle-type';
import { Unsubscribe } from 'nanoevents';
export interface LazyLoaderServiceEvents {
scriptLoadRetried: (src: string, retryCount: number) => void;
scriptLoadFailed: (src: string, error: string | Event) => void;
}
export interface LazyLoaderServiceInterface {
/**
* Bind to receive notifications about retry and failure events
*
* @template E
* @param {E} event
* @param {LazyLoaderServiceEvents[E]} callback
* @returns {Unsubscribe}
* @memberof LazyLoaderServiceInterface
*/
on<E extends keyof LazyLoaderServiceEvents>(
event: E,
callback: LazyLoaderServiceEvents[E]
): Unsubscribe;
/**
* Load a javascript bundle (module and nomodule pair)

@@ -16,6 +36,3 @@ *

*/
loadBundle(bundle: {
module?: string;
nomodule?: string;
}): Promise<Event | undefined>;
loadBundle(bundle: { module?: string; nomodule?: string }): Promise<void>;

@@ -38,4 +55,4 @@ /**

// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributes?: { key: string; value: any }[];
}): Promise<Event>;
attributes?: Record<string, string>;
}): Promise<void>;
}

@@ -1,18 +0,65 @@

import { BundleType } from './bundle-type';
import { LazyLoaderServiceInterface } from './lazy-loader-service-interface';
import { createNanoEvents, Unsubscribe } from 'nanoevents';
import type { BundleType } from './bundle-type';
import {
LazyLoaderServiceEvents,
LazyLoaderServiceInterface,
} from './lazy-loader-service-interface';
import { promisedSleep } from './promised-sleep';
export interface LazyLoaderServiceOptions {
/**
* The HTMLElement in which we put the script tags, defaults to document.head
*/
container?: HTMLElement;
/**
* The number of retries we should attempt
*/
retryCount?: number;
/**
* The retry interval in seconds
*/
retryInterval?: number;
}
export class LazyLoaderService implements LazyLoaderServiceInterface {
// the HTMLElement in which we put the script tags, defaults to document.head
private container: HTMLElement;
constructor(container: HTMLElement = document.head) {
this.container = container;
// the number of retries we should attempt
private retryCount: number;
// the retry interval in seconds
private retryInterval: number;
// the emitter for consumers to listen for events like retrying a load
private emitter = createNanoEvents<LazyLoaderServiceEvents>();
/**
* LazyLoaderService constructor
*
* @param options LazyLoaderServiceOptions
*/
constructor(options?: LazyLoaderServiceOptions) {
this.container = options?.container ?? document.head;
this.retryCount = options?.retryCount ?? 2;
this.retryInterval = options?.retryInterval ?? 1;
}
/** @inheritdoc */
loadBundle(bundle: {
on<E extends keyof LazyLoaderServiceEvents>(
event: E,
callback: LazyLoaderServiceEvents[E]
): Unsubscribe {
return this.emitter.on(event, callback);
}
/** @inheritdoc */
async loadBundle(bundle: {
module?: string;
nomodule?: string;
}): Promise<Event | undefined> {
let modulePromise: Promise<Event> | undefined;
let nomodulePromise: Promise<Event> | undefined;
}): Promise<void> {
let modulePromise: Promise<void> | undefined;
let nomodulePromise: Promise<void> | undefined;

@@ -23,3 +70,3 @@ /* istanbul ignore else */

src: bundle.module,
bundleType: BundleType.Module,
bundleType: 'module',
});

@@ -32,3 +79,3 @@ }

src: bundle.nomodule,
bundleType: BundleType.NoModule,
bundleType: 'nomodule',
});

@@ -41,9 +88,19 @@ }

/** @inheritdoc */
loadScript(options: {
async loadScript(options: {
src: string;
bundleType?: BundleType;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributes?: { key: string; value: any }[];
}): Promise<Event> {
const scriptSelector = `script[src='${options.src}'][async]`;
attributes?: Record<string, string>;
}): Promise<void> {
return this.doLoad(options);
}
private async doLoad(options: {
src: string;
bundleType?: BundleType;
attributes?: Record<string, string>;
retryCount?: number;
scriptBeingRetried?: HTMLScriptElement;
}): Promise<void> {
const retryCount = options.retryCount ?? 0;
const scriptSelector = `script[src='${options.src}'][async][retryCount='${retryCount}']`;
let script = this.container.querySelector(

@@ -53,63 +110,92 @@ scriptSelector

if (!script) {
script = document.createElement('script') as HTMLScriptElement;
script.setAttribute('src', options.src);
script.async = true;
script = this.getScriptTag(options);
this.container.appendChild(script);
}
const attributes = options.attributes ?? [];
return new Promise((resolve, reject) => {
// script has already been loaded, just resolve
if (script.getAttribute('dynamicImportLoaded')) {
resolve();
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
attributes.forEach((element: any) => {
// eslint-disable-next-line no-unused-expressions
script.setAttribute(element.key, element.value);
});
const scriptBeingRetried = options.scriptBeingRetried;
switch (options.bundleType) {
case BundleType.Module:
script.setAttribute('type', options.bundleType);
break;
// cannot be tested because modern browsers ignore `nomodule`
/* istanbul ignore next */
case BundleType.NoModule:
script.setAttribute(options.bundleType, '');
break;
default:
break;
}
}
// If multiple requests get made for this script, just stack the `onload`s
// and `onerror`s and all the callbacks will be called in-order of being received.
// If we are retrying the load, we use the `onload` / `onerror` from the script being retried
return new Promise((resolve, reject) => {
// if multiple requests get made for this script, just stack the onloads
// and onerrors and all the callbacks will be called in-order of being received
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalOnLoad: ((event: Event) => any) | null = script.onload;
const originalOnLoad: ((event: Event) => any) | null | undefined =
script.onload || scriptBeingRetried?.onload;
script.onload = (event): void => {
if (originalOnLoad) {
originalOnLoad(event);
}
originalOnLoad?.(event);
script.setAttribute('dynamicImportLoaded', 'true');
resolve(event);
resolve();
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originalOnError: ((error: string | Event) => any) | null =
script.onerror;
script.onerror = (error): void => {
if (originalOnError) {
originalOnError(error);
}
const originalOnError: OnErrorEventHandler | null | undefined =
script.onerror || scriptBeingRetried?.onerror;
/* istanbul ignore else */
if (script.parentNode) {
script.parentNode.removeChild(script);
script.onerror = async (error): Promise<void> => {
const hasBeenRetried = script.getAttribute('hasBeenRetried');
if (retryCount < this.retryCount && !hasBeenRetried) {
script.setAttribute('hasBeenRetried', 'true');
await promisedSleep(this.retryInterval * 1000);
const newRetryCount = retryCount + 1;
this.emitter.emit('scriptLoadRetried', options.src, newRetryCount);
this.doLoad({
...options,
retryCount: newRetryCount,
scriptBeingRetried: script,
});
} else {
this.emitter.emit('scriptLoadFailed', options.src, error);
originalOnError?.(error);
reject(error);
}
reject(error);
};
});
}
if (script.parentNode === null) {
this.container.appendChild(script);
} else if (script.getAttribute('dynamicImportLoaded')) {
resolve();
}
/**
* Generate a script tag with all of the proper attributes
*
* @param options
* @returns
*/
private getScriptTag(options: {
src: string;
retryCount?: number;
bundleType?: BundleType;
attributes?: Record<string, string>;
}): HTMLScriptElement {
const fixedSrc = options.src.replace("'", '"');
const script = document.createElement('script') as HTMLScriptElement;
const retryCount = options.retryCount ?? 0;
script.setAttribute('src', fixedSrc);
script.setAttribute('retryCount', retryCount.toString());
script.async = true;
const attributes = options.attributes ?? {};
Object.keys(attributes).forEach(key => {
script.setAttribute(key, attributes[key]);
});
switch (options.bundleType) {
case 'module':
script.setAttribute('type', options.bundleType);
break;
// cannot be tested because modern browsers ignore `nomodule`
/* istanbul ignore next */
case 'nomodule':
script.setAttribute(options.bundleType, '');
break;
default:
break;
}
return script;
}
}
import { expect, fixture, html } from '@open-wc/testing';
import { LazyLoaderService } from '../src/lazy-loader-service';
import { BundleType } from '../src/bundle-type';

@@ -18,3 +17,3 @@ const testServiceUrl = '/base/dist/test/test-service.js';

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
await lazyLoader.loadBundle({

@@ -33,3 +32,3 @@ module: testServiceUrl,

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });

@@ -42,19 +41,5 @@ await lazyLoader.loadScript({ src: testServiceUrl });

it('Removes the script tag if the load fails', async () => {
const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
try {
await lazyLoader.loadScript({ src: './blahblahnotfound.js' });
} catch {
// expected failure
}
const scripts = container.querySelectorAll('script');
expect(scripts.length).to.equal(0);
});
it('Only loads scripts once if called multiple times', async () => {
const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });

@@ -71,3 +56,3 @@ await lazyLoader.loadScript({ src: testServiceUrl });

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
await lazyLoader.loadScript({ src: testServiceUrl });

@@ -82,6 +67,6 @@

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
await lazyLoader.loadScript({
src: testServiceUrl,
attributes: [{ key: 'foo', value: 'bar' }],
attributes: { foo: 'bar' },
});

@@ -96,6 +81,6 @@

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });
await lazyLoader.loadScript({
src: testServiceUrl,
bundleType: BundleType.Module,
bundleType: 'module',
});

@@ -112,3 +97,3 @@

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({ container });

@@ -141,3 +126,6 @@ const count = 25;

const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService(container);
const lazyLoader = new LazyLoaderService({
container,
retryInterval: 0.1,
});

@@ -169,3 +157,89 @@ const count = 25;

});
// This is verifying that we emit an event on retry and failure.
// This allows the consumer to respond to these events with analytics handling
// anything else.
it('Emits an event when a retry occurs or fails', async () => {
const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService({
container,
retryCount: 1,
retryInterval: 0.01,
});
let testRetryCount = 0;
lazyLoader.on('scriptLoadRetried', (src: string, retryCount: number) => {
testRetryCount = retryCount;
});
let scriptLoadEventFired = false;
let failedSrc: string | undefined;
lazyLoader.on('scriptLoadFailed', (src: string) => {
scriptLoadEventFired = true;
failedSrc = src;
});
let retryFailed = false;
try {
await lazyLoader.loadScript({ src: '/base/test/blahblah.js' });
} catch (err) {
retryFailed = true;
}
expect(testRetryCount).to.equal(1);
expect(scriptLoadEventFired).to.be.true;
expect(failedSrc).to.equal('/base/test/blahblah.js');
expect(retryFailed).to.be.true;
});
it('Retries the specified number of times', async () => {
const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService({
container,
retryCount: 5,
retryInterval: 0.01,
});
try {
await lazyLoader.loadScript({ src: '/base/test/blahblah.js' });
} catch {}
const scriptTags = container.querySelectorAll('script');
expect(scriptTags.length).to.equal(6);
});
/**
* This is a special test that connects to a sidecar node server to test retrying a request.
*
* In `npm run test`, we run `test/test-server.js` while we're running our tests.
* `test-server.js` has a very specific purpose:
*
* The very first request will always return an HTTP 404, the second request returns a HTTP 200,
* then it shuts down.
*
* This lets us check that a failed request gets retried successfully.
*/
it('Can retry a reqest', async () => {
const serverUrl = 'http://localhost:5432/';
const container = (await fixture(html` <div></div> `)) as HTMLElement;
const lazyLoader = new LazyLoaderService({
container,
retryCount: 1,
retryInterval: 0.01,
});
await lazyLoader.loadScript({ src: serverUrl });
// verify we have two script tags with the expected url and a propery retryCount on each
const scriptTags = container.querySelectorAll('script');
scriptTags.forEach((tag, index) => {
expect(tag.src).to.equal(serverUrl);
expect(tag.getAttribute('retryCount'), `${index}`);
});
// verify the final load actually loaded the service
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (window as any).otherService.getResponse();
expect(result).to.equal('someotherresponse');
});
});
});

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

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