smart-load-manager 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,385 @@
1
+ import { bind, memoize } from '@exadel/esl/modules/esl-utils/decorators.js';
2
+ import { createDeferred, promisifyTimeout, promisifyEvent } from '@exadel/esl/modules/esl-utils/async.js';
3
+ import { setAttr, loadScript } from '@exadel/esl/modules/esl-utils/dom.js';
4
+
5
+ var __defProp$2 = Object.defineProperty;
6
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropSymbols$1 = Object.getOwnPropertySymbols;
8
+ var __hasOwnProp$1 = Object.prototype.hasOwnProperty;
9
+ var __propIsEnum$1 = Object.prototype.propertyIsEnumerable;
10
+ var __defNormalProp$1 = (obj, key, value) => key in obj ? __defProp$2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
+ var __spreadValues$1 = (a, b) => {
12
+ for (var prop in b || (b = {}))
13
+ if (__hasOwnProp$1.call(b, prop))
14
+ __defNormalProp$1(a, prop, b[prop]);
15
+ if (__getOwnPropSymbols$1)
16
+ for (var prop of __getOwnPropSymbols$1(b)) {
17
+ if (__propIsEnum$1.call(b, prop))
18
+ __defNormalProp$1(a, prop, b[prop]);
19
+ }
20
+ return a;
21
+ };
22
+ var __decorateClass$1 = (decorators, target, key, kind) => {
23
+ var result = __getOwnPropDesc$1(target, key) ;
24
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
25
+ if (decorator = decorators[i])
26
+ result = (decorator(target, key, result) ) || result;
27
+ if (result) __defProp$2(target, key, result);
28
+ return result;
29
+ };
30
+ var __async$2 = (__this, __arguments, generator) => {
31
+ return new Promise((resolve, reject) => {
32
+ var fulfilled = (value) => {
33
+ try {
34
+ step(generator.next(value));
35
+ } catch (e) {
36
+ reject(e);
37
+ }
38
+ };
39
+ var rejected = (value) => {
40
+ try {
41
+ step(generator.throw(value));
42
+ } catch (e) {
43
+ reject(e);
44
+ }
45
+ };
46
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
47
+ step((generator = generator.apply(__this, __arguments)).next());
48
+ });
49
+ };
50
+ function applyEarlyHints(options) {
51
+ const link = document.createElement("link");
52
+ link.rel = options.rel;
53
+ options.attrs && Object.entries(options.attrs).forEach(([name, value]) => setAttr(link, name, value));
54
+ link.href = options.href;
55
+ document.head.appendChild(link);
56
+ }
57
+ class SmartService {
58
+ constructor(_config) {
59
+ this._config = _config;
60
+ this._loaded = false;
61
+ this._mutex = Promise.resolve();
62
+ }
63
+ static config(options) {
64
+ if (options) this._config = __spreadValues$1(__spreadValues$1({}, this._config), options);
65
+ return this._config;
66
+ }
67
+ /** Create a new instance of SmartService */
68
+ static create(options) {
69
+ return new this(options);
70
+ }
71
+ /** Get the singleton instance of SmartService */
72
+ static get instance() {
73
+ if (!this._instance) {
74
+ this._instance = new this(this._config);
75
+ }
76
+ return this._instance;
77
+ }
78
+ /** Get the load method of the singleton instance */
79
+ static get load() {
80
+ return this.instance.load;
81
+ }
82
+ /** Setup Early Hints for the service */
83
+ static setupEarlyHints(options) {
84
+ return () => {
85
+ setTimeout(() => options.forEach(applyEarlyHints), 1);
86
+ return Promise.resolve();
87
+ };
88
+ }
89
+ /** Get the preload method of the singleton instance */
90
+ static get preload() {
91
+ return this.instance.preload;
92
+ }
93
+ /** Check if the service is loaded */
94
+ get isLoaded() {
95
+ return this._loaded;
96
+ }
97
+ /** Get the current mutex promise */
98
+ get mutex() {
99
+ return this._mutex;
100
+ }
101
+ /** Set a new mutex promise */
102
+ set mutex(value) {
103
+ this._debug("new Mutex: ", this._config.name, value);
104
+ this._mutex = value;
105
+ }
106
+ load() {
107
+ return __async$2(this, null, function* () {
108
+ this._debug("Service load() enter: ", this._config.name, this);
109
+ if (!this._state) {
110
+ this._state = this._loadTask();
111
+ }
112
+ return this._state;
113
+ });
114
+ }
115
+ preload() {
116
+ return __async$2(this, null, function* () {
117
+ this._debug("Service preload(): ", this._config.name);
118
+ const { url, attrs } = this._config;
119
+ if (url) setTimeout(applyEarlyHints.bind(null, { rel: "preload", href: url, attrs: __spreadValues$1({ as: "script" }, attrs) }), 1);
120
+ return Promise.resolve();
121
+ });
122
+ }
123
+ _loadTask() {
124
+ return __async$2(this, null, function* () {
125
+ try {
126
+ yield this._mutex;
127
+ this._debug("Service loading started: ", this._config.name);
128
+ if (!this._config.name) throw new Error("Service name is not specified");
129
+ if (!this._config.url) throw new Error("Service URL is not specified");
130
+ const id = (this._config.name || "").replace(/\s+/g, "").toLowerCase();
131
+ yield loadScript(`smart-${id}-script`, this._config.url, this._config.attrs);
132
+ this._onLoadScript();
133
+ return true;
134
+ } catch (e) {
135
+ this._onFailedScript(e);
136
+ return Promise.reject(false);
137
+ }
138
+ });
139
+ }
140
+ _onLoadScript() {
141
+ this._debug("Service script loaded: ", this._config.name);
142
+ this._loaded = true;
143
+ }
144
+ _onFailedScript(e) {
145
+ console.error(`Failed to load ${this._config.name} script`, e instanceof Error ? `: ${e.message}` : "");
146
+ }
147
+ _debug(...args) {
148
+ if (!this._config.debug) return;
149
+ console.log(...args);
150
+ }
151
+ }
152
+ SmartService._config = {
153
+ name: "service",
154
+ debug: false
155
+ };
156
+ __decorateClass$1([
157
+ bind
158
+ ], SmartService.prototype, "load");
159
+ __decorateClass$1([
160
+ bind
161
+ ], SmartService.prototype, "preload");
162
+
163
+ var __defProp$1 = Object.defineProperty;
164
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
165
+ var __decorateClass = (decorators, target, key, kind) => {
166
+ var result = __getOwnPropDesc(target, key) ;
167
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
168
+ if (decorator = decorators[i])
169
+ result = (decorator(target, key, result) ) || result;
170
+ if (result) __defProp$1(target, key, result);
171
+ return result;
172
+ };
173
+ var __async$1 = (__this, __arguments, generator) => {
174
+ return new Promise((resolve, reject) => {
175
+ var fulfilled = (value) => {
176
+ try {
177
+ step(generator.next(value));
178
+ } catch (e) {
179
+ reject(e);
180
+ }
181
+ };
182
+ var rejected = (value) => {
183
+ try {
184
+ step(generator.throw(value));
185
+ } catch (e) {
186
+ reject(e);
187
+ }
188
+ };
189
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
190
+ step((generator = generator.apply(__this, __arguments)).next());
191
+ });
192
+ };
193
+ function onReadyState(readyPredicate) {
194
+ return new Promise((resolve) => {
195
+ if (readyPredicate()) {
196
+ resolve();
197
+ } else {
198
+ const handler = () => {
199
+ if (readyPredicate()) {
200
+ document.removeEventListener("readystatechange", handler);
201
+ resolve();
202
+ }
203
+ };
204
+ document.addEventListener("readystatechange", handler);
205
+ }
206
+ });
207
+ }
208
+ class SmartLoad {
209
+ static defaultMutex() {
210
+ return this.createMutex(this.now);
211
+ }
212
+ static now() {
213
+ return Promise.resolve();
214
+ }
215
+ static onLoaded() {
216
+ return onReadyState(() => document.readyState !== "loading");
217
+ }
218
+ static onComplete() {
219
+ return onReadyState(() => document.readyState === "complete");
220
+ }
221
+ static createMutex(previousTask) {
222
+ return new Promise((resolve) => {
223
+ (() => __async$1(this, null, function* () {
224
+ try {
225
+ yield this._whenStarted.promise;
226
+ yield previousTask();
227
+ } catch (e) {
228
+ } finally {
229
+ resolve();
230
+ }
231
+ }))();
232
+ });
233
+ }
234
+ /** Queue a service to be loaded after the given task */
235
+ static queue(service, after = this.defaultMutex.bind(this)) {
236
+ const serviceInstance = service instanceof SmartService ? service : service.instance;
237
+ serviceInstance.mutex = this.createMutex(after);
238
+ serviceInstance.load().catch(() => void 0);
239
+ }
240
+ /** Start the smart loading process */
241
+ static start() {
242
+ this._whenStarted.resolve();
243
+ }
244
+ }
245
+ SmartLoad._whenStarted = createDeferred();
246
+ __decorateClass([
247
+ memoize()
248
+ ], SmartLoad, "defaultMutex");
249
+ __decorateClass([
250
+ memoize()
251
+ ], SmartLoad, "now");
252
+ __decorateClass([
253
+ memoize()
254
+ ], SmartLoad, "onLoaded");
255
+ __decorateClass([
256
+ memoize()
257
+ ], SmartLoad, "onComplete");
258
+
259
+ var __defProp = Object.defineProperty;
260
+ var __defProps = Object.defineProperties;
261
+ var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
262
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
263
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
264
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
265
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
266
+ var __spreadValues = (a, b) => {
267
+ for (var prop in b || (b = {}))
268
+ if (__hasOwnProp.call(b, prop))
269
+ __defNormalProp(a, prop, b[prop]);
270
+ if (__getOwnPropSymbols)
271
+ for (var prop of __getOwnPropSymbols(b)) {
272
+ if (__propIsEnum.call(b, prop))
273
+ __defNormalProp(a, prop, b[prop]);
274
+ }
275
+ return a;
276
+ };
277
+ var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
278
+ var __async = (__this, __arguments, generator) => {
279
+ return new Promise((resolve, reject) => {
280
+ var fulfilled = (value) => {
281
+ try {
282
+ step(generator.next(value));
283
+ } catch (e) {
284
+ reject(e);
285
+ }
286
+ };
287
+ var rejected = (value) => {
288
+ try {
289
+ step(generator.throw(value));
290
+ } catch (e) {
291
+ reject(e);
292
+ }
293
+ };
294
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
295
+ step((generator = generator.apply(__this, __arguments)).next());
296
+ });
297
+ };
298
+ const IDLE_THRESHOLD_DURATION = 46.6;
299
+ const IDLE_THRESHOLD_RATIO = 0.9;
300
+ const IDLE_TIMEOUT = 1e4;
301
+ function promisifyIdle(options = {}) {
302
+ var _a, _b;
303
+ const idleThresholdRatio = ((_a = options.thresholds) == null ? void 0 : _a.ratio) || IDLE_THRESHOLD_RATIO;
304
+ const idleThresholdDuration = ((_b = options.thresholds) == null ? void 0 : _b.duration) || IDLE_THRESHOLD_DURATION;
305
+ const idleTimeout = options.timeout || IDLE_TIMEOUT;
306
+ const { debug, signal } = options;
307
+ debug && performance.mark("idle.S");
308
+ return new Promise((resolve, reject) => {
309
+ const start = performance.now();
310
+ let previousFrameEnd = start;
311
+ const log = [];
312
+ const last3 = [0, 0, 0];
313
+ function finish(withResolve, reason) {
314
+ if (debug) {
315
+ console.table(log);
316
+ console.log(`Idle has ${withResolve ? "reached" : "aborted"} after ${performance.now() - start}ms`);
317
+ performance.mark("idle.E");
318
+ performance.measure("idlePromisify", "idle.S", "idle.E");
319
+ }
320
+ return withResolve ? resolve(true) : reject(reason);
321
+ }
322
+ const cb = (deadline) => {
323
+ if (signal == null ? void 0 : signal.aborted) return finish(false, new Error("Rejected by abort signal"));
324
+ const allocated = deadline.timeRemaining();
325
+ const frameEnd = performance.now() + allocated;
326
+ const frameLength = frameEnd - previousFrameEnd;
327
+ const idleRatio = allocated / frameLength;
328
+ const timeout = Math.max(0, idleTimeout - (performance.now() - start));
329
+ const isBadFrame = frameLength < 15;
330
+ if (!isBadFrame) {
331
+ last3.splice(0, 1);
332
+ last3.push(idleRatio > idleThresholdRatio ? allocated : 0);
333
+ }
334
+ const isIdleNow = last3.reduce((acc, val) => acc + val, 0) > idleThresholdDuration;
335
+ debug && !isBadFrame && log.push([timeout, frameLength, allocated, idleRatio, idleRatio > idleThresholdRatio ? 1 : 0, last3.toString()]);
336
+ if (isIdleNow || deadline.didTimeout) return finish(true);
337
+ !isBadFrame && (previousFrameEnd = frameEnd);
338
+ requestIdleCallback(cb, { timeout });
339
+ };
340
+ requestIdleCallback(cb, { timeout: idleTimeout });
341
+ });
342
+ }
343
+ function asyncSeries(tasks) {
344
+ return __async(this, null, function* () {
345
+ for (const task of tasks) {
346
+ yield task().catch();
347
+ }
348
+ });
349
+ }
350
+ function waitAny(tasks, signal) {
351
+ return (abortSignal) => __async(null, null, function* () {
352
+ const activeSignal = signal != null ? signal : abortSignal;
353
+ if (activeSignal) {
354
+ yield Promise.race(tasks.map((task) => task(activeSignal)));
355
+ return;
356
+ }
357
+ const controller = new AbortController();
358
+ try {
359
+ yield Promise.race(tasks.map((task) => task(controller.signal)));
360
+ } finally {
361
+ controller.abort();
362
+ }
363
+ });
364
+ }
365
+ function waitTimeout(timeout) {
366
+ return () => __async(null, null, function* () {
367
+ return promisifyTimeout(timeout);
368
+ });
369
+ }
370
+ const USER_ACTIVITY_EVENTS = ["keydown", "mousemove", "pointerdown", "wheel"];
371
+ function waitUserActivity() {
372
+ return (abortSignal) => __async(null, null, function* () {
373
+ yield waitAny(
374
+ USER_ACTIVITY_EVENTS.map((event) => (signal) => promisifyEvent(document, event, null, { passive: true, signal })),
375
+ abortSignal
376
+ )();
377
+ });
378
+ }
379
+ function waitIdle(options = {}) {
380
+ return (abortSignal) => __async(null, null, function* () {
381
+ return promisifyIdle(__spreadProps(__spreadValues({}, options), { signal: abortSignal }));
382
+ });
383
+ }
384
+
385
+ export { SmartLoad, SmartService, asyncSeries, promisifyIdle, waitAny, waitIdle, waitTimeout, waitUserActivity };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "smart-load-manager",
3
+ "version": "1.0.0",
4
+ "description": "A service and add-on utilities for managing third-party service (e.g., AdSense) loading for your web application that help improve Core Web Vitals.",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.cts",
8
+ "exports": {
9
+ "require": {
10
+ "types": "./dist/index.d.cts",
11
+ "default": "./dist/index.cjs"
12
+ },
13
+ "import": {
14
+ "types": "./dist/index.d.mts",
15
+ "default": "./dist/index.mjs"
16
+ }
17
+ },
18
+ "files": [
19
+ "dist"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/dshovchko/smart-services.git",
24
+ "directory": "packages/smart-load-manager"
25
+ },
26
+ "keywords": [
27
+ "loader",
28
+ "manager",
29
+ "queue",
30
+ "page-speed",
31
+ "performance",
32
+ "optimization",
33
+ "core-web-vitals"
34
+ ],
35
+ "author": "Dmytro Shovchko",
36
+ "license": "MIT",
37
+ "scripts": {
38
+ "clear": "rm -rf dist",
39
+ "build": "npm run clear && pkgroll",
40
+ "lint": "eslint \"src/**/*.ts\" --max-warnings 3",
41
+ "test": "npm run type-check && npm run lint && npm run test:run",
42
+ "test:run": "vitest run",
43
+ "test:watch": "vitest watch",
44
+ "type-check": "tsc --noEmit"
45
+ },
46
+ "peerDependencies": {
47
+ "@exadel/esl": "^5.15.0"
48
+ }
49
+ }