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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Dmytro Shovchko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,334 @@
1
+ # smart-load-manager
2
+
3
+ A queued loader and utility toolkit for third-party scripts. Provides a configurable `SmartService` base class, a cooperative `SmartLoad` queue, and tiny helpers for defer/idle orchestration.
4
+
5
+ ## Features
6
+
7
+ - **SmartService** - declarative config, mutexed `load()`, `preload()` early hints, optional debug logging.
8
+ - **SmartLoad** - cooperative queue with `whenReady`/`onComplete` hooks and mutex binding out of the box.
9
+ - **Helpers** - `waitAny`, `waitTimeout`, `waitUserActivity`, `waitIdle`, `asyncSeries` to orchestrate retries, idle windows, or user-driven triggers.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install smart-load-manager @exadel/esl
15
+ ```
16
+
17
+ `smart-load-manager` relies on [`@exadel/esl`](https://www.npmjs.com/package/@exadel/esl) as a peer dependency (you can learn more about ESL on the library [website](https://esl-ui.com/)) for decorators and async helpers. Modern package managers will warn if a compatible ESL version is missing; if your project already includes ESL, you can omit it from the command.
18
+
19
+ ## Quick start
20
+
21
+ ```ts
22
+ import {
23
+ SmartService,
24
+ SmartLoad,
25
+ waitAny,
26
+ waitTimeout,
27
+ waitUserActivity
28
+ } from 'smart-load-manager';
29
+
30
+ const consent = SmartService.create({name: 'Consent', url: 'https://cdn.example.com/consent.js'});
31
+ const analytics = SmartService.create({name: 'Analytics', url: 'https://cdn.example.com/analytics.js'});
32
+ const ads = SmartService.create({name: 'Ads', url: 'https://cdn.example.com/ads.js'});
33
+
34
+ const waitIntent = waitAny([
35
+ waitUserActivity(),
36
+ waitTimeout(2000)
37
+ ]);
38
+
39
+ SmartLoad.queue(consent); // baseline dependency
40
+ SmartLoad.queue(analytics, consent.load); // analytics follows consent
41
+ SmartLoad.queue(ads, async () => {
42
+ await waitIntent(); // ads wait for user activity or fallback timeout
43
+ });
44
+ SmartLoad.start();
45
+ ```
46
+
47
+ Each service is configured once, then queue them behind the dependency (or helper) they care about, and let `SmartLoad` serialize the actual script injections. The helpers keep the orchestration declarative—here ads only load after either the user interacts or a timeout fires.
48
+
49
+ ## Configuration
50
+
51
+ `SmartServiceOptions` accepted by `SmartService.config()` / `SmartLoad.create()`:
52
+
53
+ | Option | Type | Description |
54
+ | --- | --- | --- |
55
+ | `name` | `string` | Identifier used for logging + script element id. Required. |
56
+ | `url` | `string` | Remote script URL. Required for `load()`. |
57
+ | `attrs` | `LoadScriptAttributes` | Attributes passed to underlying `<script>` tag (e.g. `async`, `crossorigin`). |
58
+ | `debug` | `boolean` | Enables verbose console output for service lifecycle + mutex transitions. |
59
+
60
+ **Early hints**
61
+
62
+ ```ts
63
+ SmartService.setupEarlyHints([
64
+ {rel: 'preconnect', href: 'https://pagead2.googlesyndication.com'},
65
+ {rel: 'dns-prefetch', href: 'https://googleads.g.doubleclick.net'}
66
+ ]);
67
+ ```
68
+
69
+ Call the returned function during application bootstrap to queue `<link rel="...">` elements with a minimal delay.
70
+
71
+ Each entry accepts:
72
+
73
+ | Field | Type | Notes |
74
+ | --- | --- | --- |
75
+ | `rel` | `'dns-prefetch' | 'preconnect' | 'prefetch' | 'preload' | 'prerender'` | Standard link relationship. |
76
+ | `href` | `string` | Target origin/asset. |
77
+ | `attrs.as` | `string` | Optional `as` value for preload/prefetch. |
78
+ | `attrs.crossorigin` | `string \| boolean \| null` | Mirrors the `<link crossorigin>` attribute.
79
+
80
+ ### Preloading service scripts
81
+
82
+ `SmartService.preload()` drops a matching `<link rel="preload" as="script">` tag for the configured URL. Use it when you already know that a service will be needed, but you would like to defer `load()` until after user input, consent, or idle time:
83
+
84
+ ```ts
85
+ const analytics = SmartService.create({
86
+ name: 'Analytics',
87
+ url: 'https://cdn.example.com/analytics.js',
88
+ attrs: {crossorigin: 'anonymous'}
89
+ });
90
+
91
+ const registerHints = SmartService.setupEarlyHints([
92
+ {rel: 'preconnect', href: 'https://cdn.example.com'},
93
+ {rel: 'dns-prefetch', href: 'https://cdn.example.com'}
94
+ ]);
95
+
96
+ await registerHints(); // schedule <link rel="preconnect"> tags during bootstrap
97
+ await analytics.preload(); // enqueue <link rel="preload" as="script">
98
+ // later you can await analytics.load() once prerequisites are met
99
+ ```
100
+
101
+ Call `setupEarlyHints()` at application bootstrap (or server-side) so connections warm up while the rest of the UI initializes. Then call `preload()` close to the navigation or route transition that will eventually call `load()`. Both helpers execute quickly and are safe to reuse inside orchestrated queues.
102
+
103
+ ## SmartLoad usage
104
+
105
+ This example adds consent loading to the queue of orchestration, which should start downloading immediately.
106
+
107
+ ```ts
108
+ import {SmartLoad, SmartService} from 'smart-load-manager';
109
+
110
+ const consent = SmartService.create({name: 'Consent', url: 'https://cdn.example.com/consent.js'});
111
+ SmartLoad.queue(consent); // same as SmartLoad.queue(consent, SmartLoad.now())
112
+ SmartLoad.start();
113
+ ```
114
+
115
+ This example adds consent loading to the orchestration queue, which should start downloading after the document is at least interactive.
116
+
117
+ ```ts
118
+ import {SmartLoad, SmartService} from 'smart-load-manager';
119
+
120
+ const consent = SmartService.create({name: 'Consent', url: 'https://cdn.example.com/consent.js'});
121
+ SmartLoad.queue(consent, SmartLoad.onLoaded());
122
+ SmartLoad.start();
123
+ ```
124
+
125
+ This example adds consent loading to the orchestration queue, which should start downloading after the document is fully loaded.
126
+
127
+ ```ts
128
+ import {SmartLoad, SmartService} from 'smart-load-manager';
129
+
130
+ const consent = SmartService.create({name: 'Consent', url: 'https://cdn.example.com/consent.js'});
131
+ SmartLoad.queue(consent, SmartLoad.onComplete());
132
+ SmartLoad.start();
133
+ ```
134
+
135
+ ### Orchestrating several services
136
+
137
+ The core idea is to queue every third-party integration behind the dependency it needs. A typical flow:
138
+
139
+ ```ts
140
+ import {SmartService, SmartLoad, asyncSeries, waitAny, waitTimeout, waitUserActivity} from 'smart-load-manager';
141
+
142
+ const consent = SmartService.create({name: 'Consent', url: 'https://cdn.example.com/consent.js'});
143
+ const tagManager = SmartService.create({name: 'TagManager', url: 'https://www.googletagmanager.com/gtag/js?id=G-XXXX'});
144
+ const analytics = SmartService.create({name: 'Analytics', url: '/analytics.js'});
145
+ const fallback = SmartService.create({name: 'FallbackAds'});
146
+
147
+ // Queue core services
148
+ SmartLoad.queue(consent, SmartLoad.onComplete()); // runs once the document is fully loaded
149
+ SmartLoad.queue(tagManager, consent.load); // runs once consent is loaded
150
+ SmartLoad.queue(analytics, tagManager.load); // runs once TagManager is loaded
151
+
152
+ // Compose richer logic via helpers
153
+ SmartLoad.queue(fallback, async () => await asyncSeries([
154
+ analytics.load,
155
+ waitAny([waitTimeout(2000), waitUserActivity()])
156
+ ])
157
+ ); // runs once seriesly completed analytics loading and pass 2s or user interacts with the page
158
+
159
+ SmartLoad.start();
160
+ ```
161
+
162
+ - `SmartLoad.queue(service, after?)` links each service to a promise factory (`after`) returned from the previous ones. It can be either `otherService.load`, a custom async function, or helpers such as `SmartLoad.onComplete`.
163
+ - `SmartLoad.start()` resolves the internal deferred so the queue begins executing. Until then you can register every dependency declaratively.
164
+ - Each service still exposes `load()` for direct use if you need to branch on success/failure.
165
+
166
+ #### Advanced preload + idle pipeline
167
+
168
+ Pair `setupEarlyHints()`, `preload()`, and helper waits when you want to warm up the network aggressively but still defer execution to calmer windows. The example below preconnects to several vendors, issues preloads during the first idle slot, and only then loads marketing scripts once analytics settles (or a manual timeout elapses):
169
+
170
+ ```ts
171
+ import {
172
+ SmartService,
173
+ SmartLoad,
174
+ asyncSeries,
175
+ waitIdle,
176
+ waitAny,
177
+ waitTimeout
178
+ } from 'smart-load-manager';
179
+
180
+ const analytics = SmartService.create({name: 'Analytics', url: 'https://cdn.analytics.example/app.js'});
181
+ const marketing = SmartService.create({name: 'Marketing', url: 'https://cdn.marketing.example/pixel.js'});
182
+ const ads = SmartService.create({name: 'Ads', url: 'https://ads.example.net/loader.js'});
183
+
184
+ SmartLoad.queue(analytics, async () => await asyncSeries([
185
+ analytics.preload,
186
+ SmartService.setupEarlyHints([
187
+ {rel: 'preconnect', href: 'https://cdn.marketing.example'},
188
+ {rel: 'preconnect', href: 'https://ads.example.net'}
189
+ ]),
190
+ waitAny([
191
+ waitIdle({thresholds: {duration: 80}, timeout: 5000}),
192
+ waitUserActivity()
193
+ ])
194
+ ])
195
+ );
196
+
197
+ SmartLoad.queue(marketing, async () => await asyncSeries([
198
+ analytics.load,
199
+ marketing.preload,
200
+ waitTimeout(300)
201
+ ])
202
+ );
203
+
204
+ SmartLoad.queue(ads, async () => await asyncSeries([
205
+ marketing.load,
206
+ ads.preload,
207
+ SmartService.setupEarlyHints([
208
+ {rel: 'preconnect', href: 'https://fonts.googleapis.com'},
209
+ {rel: 'preconnect', href: 'https://fonts.gstatic.com', attrs: {crossorigin: ''}}
210
+ ])
211
+ ])
212
+ );
213
+
214
+ SmartLoad.start();
215
+ marketing.load().catch(() => console.log('?> Marketing failed! Will do something else...'));
216
+ ```
217
+
218
+ This load-orchestration example demonstrates the following: three services- analytics, marketing, and ads - are loaded. Analytics will start loading after the following chain of actions is completed:
219
+ - preloading of analytics service
220
+ - preconnecting to https://cdn.marketing.example and https://ads.example.net
221
+ - waiting for user activity or browsers idle state (something that will happen first)
222
+ - after that starts analytics service loading, evaluation and execution
223
+ Marketing will start loading after the following chain of actions is completed:
224
+ - loading analytics service
225
+ - preloading of marketing service
226
+ - passing 300msec
227
+ Ads will start loading after the following chain of actions is completed:
228
+ - loading marketing service
229
+ - preloading of marketing service
230
+ - preconnecting to Google Fonts
231
+
232
+ `SmartLoad.queue()` triggers the service's `load()` after your `after` hook resolves, so the hook should only coordinate prerequisites. Never call `load()` yourself until you have configured the queue and started it by calling `start()`. Otherwise, your orchestration will not work properly.
233
+ Once you have configured the queue and started it, you can monitor the loading results. Each `load()` for direct use if you need to branch on success/failure.
234
+
235
+ ## Helper recipes
236
+
237
+ ### `asyncSeries` helper
238
+
239
+ `asyncSeries(tasks)` executes a list of async factories one after another, ignoring individual rejections so the rest of the pipeline can continue. Handy when preparing services inside `SmartLoad.queue()` hooks:
240
+
241
+ ```ts
242
+ import {asyncSeries, waitIdle, waitTimeout} from 'smart-load-manager';
243
+
244
+ await asyncSeries([
245
+ service.preload,
246
+ waitIdle({timeout: 4000}),
247
+ waitTimeout(250)
248
+ ]);
249
+ ```
250
+
251
+ Each item is a function returning a promise, which keeps the helpers lazily evaluated and compatible with bound service methods.
252
+
253
+ ### `waitAny` helper
254
+
255
+ `waitAny(tasks)` composes several wait tasks and resolves when the first one finishes. This is useful for racing user intent against timeouts or DOM readiness gates. Every task receives an `AbortSignal`, so the remaining ones cancel automatically:
256
+
257
+ ```ts
258
+ import {waitAny, waitTimeout, waitUserActivity} from 'smart-load-manager';
259
+
260
+ const waitForIntent = waitAny([
261
+ waitUserActivity(),
262
+ waitTimeout(4000)
263
+ ]);
264
+
265
+ await waitForIntent();
266
+ ```
267
+
268
+ ### `waitIdle` helper
269
+
270
+ `waitIdle(options)` uses `promisifyIdle` under the hood and exposes a wait task that resolves when the browser accumulates enough idle time. Drop it into `waitAny()` races or `asyncSeries()` steps whenever you need deterministic "browser is calm" checks before kicking off hydration, A/B scripts, etc.
271
+
272
+ | Option | Type | Description |
273
+ | --- | --- | --- |
274
+ | `thresholds.duration` | `number` | Desired sum of idle milliseconds across recent frames (default `46.6`). |
275
+ | `thresholds.ratio` | `number` | Ratio of idle time to frame time (default `0.9`). |
276
+ | `timeout` | `number` | Maximum wait before resolving/forcing idle (default `10000ms`). |
277
+ | `debug` | `boolean` | Dumps frame metrics and `performance.measure` markers. |
278
+ | `signal` | `AbortSignal` | Cancels idle waiting.
279
+
280
+ ### `waitTimeout` helper
281
+
282
+ `waitTimeout(ms)` returns a wait task that resolves after the given milliseconds. Compose it with `waitAny()` or `asyncSeries()` when you need guardrails around long-running operations:
283
+
284
+ ```ts
285
+ import {waitTimeout} from 'smart-load-manager';
286
+
287
+ await waitTimeout(2500)();
288
+ ```
289
+
290
+ ### `waitUserActivity` helper
291
+
292
+ `waitUserActivity()` listens for key, pointer, wheel, or mouse movement events and resolves once the user interacts with the page. The listener uses passive handlers and cleans up automatically via `AbortController`:
293
+
294
+ ```ts
295
+ import {waitUserActivity, waitAny, waitTimeout} from 'smart-load-manager';
296
+
297
+ const waitForEngagement = waitAny([
298
+ waitUserActivity(),
299
+ waitTimeout(8000)
300
+ ]);
301
+
302
+ await waitForEngagement();
303
+ ```
304
+
305
+ ### `promisifyIdle` helper
306
+
307
+ `promisifyIdle(options)` exposes the underlying idle detection primitive used by `waitIdle()`. It returns a promise that resolves once the recent frames accumulate enough idle budget (or rejects if an abort signal fires). Use it when you need bespoke coordination outside the wait-task helpers—for example, pausing until the browser sits idle before running custom logic:
308
+
309
+ ```ts
310
+ import {promisifyIdle} from 'smart-load-manager';
311
+
312
+ await promisifyIdle({
313
+ thresholds: {duration: 60, ratio: 0.85},
314
+ timeout: 6000,
315
+ debug: true
316
+ });
317
+
318
+ // safe to run heavier DOM work here
319
+ ```
320
+
321
+ All option fields mirror those listed for `waitIdle()`, with `signal` allowing you to cancel the wait early via `AbortController`.
322
+
323
+ | Option | Type | Purpose |
324
+ | --- | --- | --- |
325
+ | `thresholds.duration` | `number` | Total milliseconds of "good" idle time the browser must accumulate across recent frames before resolving. Increase it when you want longer calm periods. |
326
+ | `thresholds.ratio` | `number` | Minimum ratio of idle time to total frame time (0-1). Lower this if your app runs heavier animation but you still want to treat short gaps as idle. |
327
+ | `timeout` | `number` | Hard stop in milliseconds; once reached, the promise resolves even if thresholds were not met. Useful to avoid blocking your queue forever. |
328
+ | `debug` | `boolean` | Prints a console table with per-frame metrics and creates `performance.measure` marks, helping you tune thresholds. |
329
+ | `signal` | `AbortSignal` | Abort the wait from the outside (e.g., when the user navigates away or you no longer need the service). |
330
+
331
+ ## Compatibility & performance
332
+
333
+ - **Browser support**: Targets ES2019+ runtimes with native `Promise`, async/await, and `AbortController`. Helpers such as `waitIdle()` rely on `requestIdleCallback`/`performance` APIs; polyfill them (plus `AbortController` if needed) before calling `SmartService.setupEarlyHints()` or `SmartLoad.start()` when supporting legacy browsers.
334
+ - **Performance impact**: `SmartLoad.queue()` serializes third-party script injections, so only one network fetch and evaluation pipeline runs at a time. Pairing the queue with helpers like `waitIdle()`, `waitUserActivity()`, and `asyncSeries()` lets you gate each integration on idle frames or intent, cutting down layout shifts, main-thread contention, and aggregate blocking time.