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 +21 -0
- package/README.md +334 -0
- package/dist/index.cjs +394 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.mts +144 -0
- package/dist/index.mjs +385 -0
- package/package.json +49 -0
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.
|