react-fastload 0.1.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.cjs ADDED
@@ -0,0 +1,1348 @@
1
+ 'use strict';
2
+
3
+ var React5 = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
7
+
8
+ var React5__default = /*#__PURE__*/_interopDefault(React5);
9
+
10
+ // src/components/FastLoadProvider.tsx
11
+
12
+ // src/core/PriorityEngine.ts
13
+ var PRIORITY_ORDER = {
14
+ CRITICAL: 0,
15
+ HIGH: 1,
16
+ NORMAL: 2,
17
+ LOW: 3,
18
+ IDLE: 4
19
+ };
20
+ var PriorityEngine = class {
21
+ /**
22
+ * Lower score = loaded sooner. CRITICAL always sorts first regardless
23
+ * of other signals, per the "never delay explicitly critical resources"
24
+ * requirement.
25
+ */
26
+ score(resource, ctx) {
27
+ if (resource.priority === "CRITICAL") return -Infinity;
28
+ let score = PRIORITY_ORDER[resource.priority] * 1e3;
29
+ const distance = resource.viewportDistance;
30
+ if (typeof distance === "number") {
31
+ score += Math.max(distance, 0) / 10;
32
+ } else {
33
+ score += 500;
34
+ }
35
+ if (this.isConstrainedConnection(ctx.connection)) {
36
+ if (resource.priority === "LOW" || resource.priority === "IDLE") {
37
+ score += 2e3;
38
+ }
39
+ }
40
+ return score;
41
+ }
42
+ compare(a, b, ctx) {
43
+ return this.score(a, ctx) - this.score(b, ctx);
44
+ }
45
+ isConstrainedConnection(connection) {
46
+ if (connection.saveData) return true;
47
+ if (!connection.supported) return false;
48
+ if (connection.effectiveType === "slow-2g" || connection.effectiveType === "2g") {
49
+ return true;
50
+ }
51
+ if (typeof connection.downlink === "number" && connection.downlink > 0 && connection.downlink < 1.5) {
52
+ return true;
53
+ }
54
+ return false;
55
+ }
56
+ /** Whether a resource sitting at `distance` px from the viewport should become eligible. */
57
+ isWithinPreloadZone(distance, preloadDistance) {
58
+ if (typeof distance !== "number") return false;
59
+ return distance <= preloadDistance;
60
+ }
61
+ };
62
+
63
+ // src/core/ResourceRegistry.ts
64
+ var ResourceRegistry = class {
65
+ constructor() {
66
+ this.resources = /* @__PURE__ */ new Map();
67
+ this.listeners = /* @__PURE__ */ new Set();
68
+ }
69
+ register(input) {
70
+ const existing = this.resources.get(input.id);
71
+ if (existing) {
72
+ return existing;
73
+ }
74
+ const record = {
75
+ id: input.id,
76
+ type: input.type,
77
+ priority: input.priority,
78
+ strategy: input.strategy,
79
+ state: "idle",
80
+ estimatedSize: input.estimatedSize,
81
+ meta: input.meta,
82
+ deferred: false,
83
+ prefetched: false,
84
+ abortController: typeof AbortController !== "undefined" ? new AbortController() : void 0,
85
+ timestamps: {
86
+ registeredAt: now()
87
+ }
88
+ };
89
+ this.resources.set(input.id, record);
90
+ this.emit();
91
+ return record;
92
+ }
93
+ unregister(id) {
94
+ if (this.resources.delete(id)) {
95
+ this.emit();
96
+ }
97
+ }
98
+ get(id) {
99
+ return this.resources.get(id);
100
+ }
101
+ has(id) {
102
+ return this.resources.has(id);
103
+ }
104
+ all() {
105
+ return Array.from(this.resources.values());
106
+ }
107
+ byState(state) {
108
+ return this.all().filter((r) => r.state === state);
109
+ }
110
+ update(id, patch) {
111
+ const record = this.resources.get(id);
112
+ if (!record) return void 0;
113
+ const updated = {
114
+ ...record,
115
+ ...patch,
116
+ timestamps: { ...record.timestamps, ...patch.timestamps }
117
+ };
118
+ this.resources.set(id, updated);
119
+ this.emit();
120
+ return updated;
121
+ }
122
+ /**
123
+ * Returns the CURRENT record after the transition — not the stale
124
+ * pre-call reference. This matters: a caller that does
125
+ * `const r = registry.setState(id, "eligible")` must see `r.state ===
126
+ * "eligible"`, not whatever `r` looked like before this call, since
127
+ * `update()` replaces the Map entry with a new object rather than
128
+ * mutating the old one in place. (This exact mismatch was a real bug
129
+ * in LoadManager.register(), caught by a clean-install runtime smoke
130
+ * test — see CHANGELOG.) Returns `undefined` only if `id` isn't
131
+ * registered at all.
132
+ */
133
+ setState(id, state) {
134
+ const record = this.resources.get(id);
135
+ if (!record) return void 0;
136
+ if (record.state === state) return record;
137
+ const timestamps = { ...record.timestamps };
138
+ const t = now();
139
+ if (state === "eligible" && !timestamps.eligibleAt) timestamps.eligibleAt = t;
140
+ if (state === "loading" && !timestamps.loadStartedAt) timestamps.loadStartedAt = t;
141
+ if ((state === "loaded" || state === "error") && !timestamps.loadEndedAt) {
142
+ timestamps.loadEndedAt = t;
143
+ }
144
+ return this.update(id, { state, timestamps });
145
+ }
146
+ setViewportDistance(id, distance) {
147
+ this.update(id, { viewportDistance: distance });
148
+ }
149
+ markDeferred(id) {
150
+ this.update(id, { deferred: true });
151
+ }
152
+ markPrefetched(id) {
153
+ this.update(id, { prefetched: true });
154
+ }
155
+ subscribe(listener) {
156
+ this.listeners.add(listener);
157
+ return () => this.listeners.delete(listener);
158
+ }
159
+ clear() {
160
+ this.resources.clear();
161
+ this.emit();
162
+ }
163
+ /** Aggregate counters used by the metrics layer. Pure derived data, no side effects. */
164
+ summary() {
165
+ var _a;
166
+ const all = this.all();
167
+ let deferredRequests = 0;
168
+ let bytesDeferred = 0;
169
+ let imagesDeferred = 0;
170
+ let videosDeferred = 0;
171
+ let audiosDeferred = 0;
172
+ let componentsDeferred = 0;
173
+ for (const r of all) {
174
+ if (r.deferred) {
175
+ deferredRequests += 1;
176
+ bytesDeferred += (_a = r.estimatedSize) != null ? _a : 0;
177
+ if (r.type === "image") imagesDeferred += 1;
178
+ if (r.type === "video") videosDeferred += 1;
179
+ if (r.type === "audio") audiosDeferred += 1;
180
+ if (r.type === "component") componentsDeferred += 1;
181
+ }
182
+ }
183
+ return {
184
+ totalRequests: all.length,
185
+ initialRequests: all.filter((r) => !r.deferred).length,
186
+ deferredRequests,
187
+ deferredResources: deferredRequests,
188
+ bytesDeferred,
189
+ imagesDeferred,
190
+ videosDeferred,
191
+ audiosDeferred,
192
+ componentsDeferred
193
+ };
194
+ }
195
+ emit() {
196
+ const snapshot = this.all();
197
+ for (const listener of this.listeners) listener(snapshot);
198
+ }
199
+ };
200
+ function now() {
201
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
202
+ }
203
+
204
+ // src/core/Scheduler.ts
205
+ var Scheduler = class {
206
+ constructor(registry, priorityEngine, options = {}) {
207
+ this.registry = registry;
208
+ this.priorityEngine = priorityEngine;
209
+ this.activeCount = 0;
210
+ this.loaders = /* @__PURE__ */ new Map();
211
+ this.decisions = [];
212
+ this.maxDecisionLog = 200;
213
+ this.scheduled = false;
214
+ var _a;
215
+ this.concurrency = (_a = options.concurrency) != null ? _a : 4;
216
+ this.onDecision = options.onDecision;
217
+ }
218
+ /** Register the function used to actually load a given resource id (called once per resource). */
219
+ registerLoader(resourceId, loader) {
220
+ this.loaders.set(resourceId, loader);
221
+ }
222
+ unregisterLoader(resourceId) {
223
+ this.loaders.delete(resourceId);
224
+ }
225
+ /**
226
+ * Request a scheduling pass. Coalesced into a microtask so multiple
227
+ * rapid registry updates (e.g. many IntersectionObserver callbacks in
228
+ * one frame) only trigger a single pass.
229
+ */
230
+ requestPass(ctx) {
231
+ if (this.scheduled) return;
232
+ this.scheduled = true;
233
+ queueMicrotask(() => {
234
+ this.scheduled = false;
235
+ this.runPass({ ctx });
236
+ });
237
+ }
238
+ /** Synchronous scheduling pass, exposed directly for tests and advanced use. */
239
+ runPass({ ctx }) {
240
+ const eligible = this.registry.byState("eligible").slice().sort((a, b) => this.priorityEngine.compare(a, b, ctx));
241
+ let freeSlots = this.concurrency - this.activeCount;
242
+ if (freeSlots <= 0) return;
243
+ for (const resource of eligible) {
244
+ if (freeSlots <= 0) break;
245
+ const loader = this.loaders.get(resource.id);
246
+ if (!loader) continue;
247
+ freeSlots -= 1;
248
+ this.dispatch(resource, loader);
249
+ }
250
+ }
251
+ dispatch(resource, loader) {
252
+ var _a;
253
+ this.activeCount += 1;
254
+ this.registry.setState(resource.id, "loading");
255
+ this.record({
256
+ resourceId: resource.id,
257
+ action: "load",
258
+ reason: `priority=${resource.priority} distance=${(_a = resource.viewportDistance) != null ? _a : "unknown"}`,
259
+ timestamp: nowMs()
260
+ });
261
+ loader(resource).then(() => {
262
+ this.registry.setState(resource.id, "loaded");
263
+ }).catch(() => {
264
+ this.registry.setState(resource.id, "error");
265
+ }).finally(() => {
266
+ this.activeCount -= 1;
267
+ this.unregisterLoader(resource.id);
268
+ });
269
+ }
270
+ /** Explicitly defer a resource (used by observers when it leaves the preload zone again, or on slow connections). */
271
+ defer(resourceId, reason) {
272
+ this.registry.markDeferred(resourceId);
273
+ this.record({ resourceId, action: "defer", reason, timestamp: nowMs() });
274
+ }
275
+ prefetch(resourceId, reason) {
276
+ this.registry.markPrefetched(resourceId);
277
+ this.record({ resourceId, action: "prefetch", reason, timestamp: nowMs() });
278
+ }
279
+ getDecisionLog() {
280
+ return this.decisions.slice();
281
+ }
282
+ record(decision) {
283
+ var _a;
284
+ this.decisions.push(decision);
285
+ if (this.decisions.length > this.maxDecisionLog) {
286
+ this.decisions.shift();
287
+ }
288
+ (_a = this.onDecision) == null ? void 0 : _a.call(this, decision);
289
+ }
290
+ };
291
+ function nowMs() {
292
+ return typeof performance !== "undefined" ? performance.now() : Date.now();
293
+ }
294
+
295
+ // src/utils/browserSupport.ts
296
+ var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
297
+ function supportsIntersectionObserver() {
298
+ return isBrowser && typeof window.IntersectionObserver === "function";
299
+ }
300
+ function supportsPerformanceObserver() {
301
+ return isBrowser && typeof PerformanceObserver !== "undefined";
302
+ }
303
+ function supportsNavigationTiming() {
304
+ return isBrowser && typeof performance !== "undefined" && typeof performance.getEntriesByType === "function";
305
+ }
306
+ function supportsNetworkInformation() {
307
+ return isBrowser && "connection" in navigator;
308
+ }
309
+ function supportsFetchPriority() {
310
+ if (!isBrowser) return false;
311
+ const img = document.createElement("img");
312
+ return "fetchPriority" in img;
313
+ }
314
+
315
+ // src/observers/ViewportObserver.ts
316
+ var ViewportObserver = class {
317
+ constructor(preloadDistance = 1e3) {
318
+ this.callbacks = /* @__PURE__ */ new Map();
319
+ this.preloadDistance = preloadDistance;
320
+ if (supportsIntersectionObserver()) {
321
+ this.observer = new IntersectionObserver(
322
+ (entries) => this.handleEntries(entries),
323
+ {
324
+ root: null,
325
+ rootMargin: `${preloadDistance}px`,
326
+ threshold: [0, 0.01, 0.5, 1]
327
+ }
328
+ );
329
+ }
330
+ }
331
+ observe(element, callback) {
332
+ this.callbacks.set(element, callback);
333
+ if (!this.observer) {
334
+ callback(0, true);
335
+ return;
336
+ }
337
+ this.observer.observe(element);
338
+ }
339
+ unobserve(element) {
340
+ var _a;
341
+ this.callbacks.delete(element);
342
+ (_a = this.observer) == null ? void 0 : _a.unobserve(element);
343
+ }
344
+ disconnect() {
345
+ var _a;
346
+ (_a = this.observer) == null ? void 0 : _a.disconnect();
347
+ this.callbacks.clear();
348
+ }
349
+ handleEntries(entries) {
350
+ for (const entry of entries) {
351
+ const callback = this.callbacks.get(entry.target);
352
+ if (!callback) continue;
353
+ const distance = estimateDistance(entry);
354
+ callback(distance, entry.isIntersecting);
355
+ }
356
+ }
357
+ };
358
+ function estimateDistance(entry) {
359
+ if (entry.isIntersecting) return 0;
360
+ const target = entry.boundingClientRect;
361
+ const root = entry.rootBounds;
362
+ if (!root) {
363
+ return Number.POSITIVE_INFINITY;
364
+ }
365
+ if (target.bottom < root.top) return root.top - target.bottom;
366
+ if (target.top > root.bottom) return target.top - root.bottom;
367
+ if (target.right < root.left) return root.left - target.right;
368
+ if (target.left > root.right) return target.left - root.right;
369
+ return 0;
370
+ }
371
+
372
+ // src/utils/connection.ts
373
+ function getConnectionApi() {
374
+ var _a, _b;
375
+ if (!isBrowser) return void 0;
376
+ const nav = navigator;
377
+ return (_b = (_a = nav.connection) != null ? _a : nav.mozConnection) != null ? _b : nav.webkitConnection;
378
+ }
379
+ function getConnectionInfo() {
380
+ var _a, _b;
381
+ const conn = getConnectionApi();
382
+ const supported = supportsNetworkInformation() && !!conn;
383
+ if (!supported || !conn) {
384
+ return {
385
+ effectiveType: null,
386
+ downlink: null,
387
+ rtt: null,
388
+ saveData: false,
389
+ supported: false
390
+ };
391
+ }
392
+ return {
393
+ effectiveType: (_a = conn.effectiveType) != null ? _a : null,
394
+ downlink: typeof conn.downlink === "number" ? conn.downlink : null,
395
+ rtt: typeof conn.rtt === "number" ? conn.rtt : null,
396
+ saveData: (_b = conn.saveData) != null ? _b : false,
397
+ supported: true
398
+ };
399
+ }
400
+ function subscribeToConnectionChange(onChange) {
401
+ const conn = getConnectionApi();
402
+ if (!conn || typeof conn.addEventListener !== "function") {
403
+ return () => {
404
+ };
405
+ }
406
+ const handler = () => onChange(getConnectionInfo());
407
+ conn.addEventListener("change", handler);
408
+ return () => {
409
+ var _a;
410
+ return (_a = conn.removeEventListener) == null ? void 0 : _a.call(conn, "change", handler);
411
+ };
412
+ }
413
+
414
+ // src/core/LoadManager.ts
415
+ var LoadManager = class {
416
+ constructor(options = {}) {
417
+ this.refCounts = /* @__PURE__ */ new Map();
418
+ var _a, _b, _c;
419
+ this.preloadDistance = (_a = options.preloadDistance) != null ? _a : 1e3;
420
+ this.debug = (_b = options.debug) != null ? _b : false;
421
+ this.registry = new ResourceRegistry();
422
+ this.priorityEngine = new PriorityEngine();
423
+ this.viewportObserver = new ViewportObserver(this.preloadDistance);
424
+ this.connection = getConnectionInfo();
425
+ this.scheduler = new Scheduler(this.registry, this.priorityEngine, {
426
+ concurrency: (_c = options.concurrency) != null ? _c : 4,
427
+ onDecision: this.debug ? (d) => this.logDecision(d) : void 0
428
+ });
429
+ this.unsubscribeConnection = subscribeToConnectionChange((info) => {
430
+ this.connection = info;
431
+ this.requestPass();
432
+ });
433
+ }
434
+ register(input) {
435
+ var _a, _b;
436
+ const refCount = ((_a = this.refCounts.get(input.id)) != null ? _a : 0) + 1;
437
+ this.refCounts.set(input.id, refCount);
438
+ let record = this.registry.register(input);
439
+ if (input.strategy === "eager" || input.priority === "CRITICAL") {
440
+ record = (_b = this.registry.setState(record.id, "eligible")) != null ? _b : record;
441
+ this.requestPass();
442
+ }
443
+ return record;
444
+ }
445
+ /**
446
+ * Decrements the id's reference count. Only when it reaches zero — i.e.
447
+ * the last mounted consumer of this resource has unmounted — does this
448
+ * actually abort any in-flight load, remove the scheduler's loader, and
449
+ * delete the registry entry. See the class doc for why.
450
+ */
451
+ unregister(id) {
452
+ var _a, _b;
453
+ const remaining = ((_a = this.refCounts.get(id)) != null ? _a : 1) - 1;
454
+ if (remaining > 0) {
455
+ this.refCounts.set(id, remaining);
456
+ return;
457
+ }
458
+ this.refCounts.delete(id);
459
+ const record = this.registry.get(id);
460
+ if (record && record.state !== "loaded" && record.state !== "error") {
461
+ (_b = record.abortController) == null ? void 0 : _b.abort();
462
+ }
463
+ this.scheduler.unregisterLoader(id);
464
+ this.registry.unregister(id);
465
+ }
466
+ setLoader(id, loader) {
467
+ this.scheduler.registerLoader(id, loader);
468
+ this.requestPass();
469
+ }
470
+ /**
471
+ * Attaches the shared ViewportObserver to an element for a given
472
+ * resource id. Returns an unobserve function. A no-op (returning a
473
+ * no-op cleanup) for resources that skip viewport gating — callers
474
+ * don't need to branch on that themselves.
475
+ */
476
+ observeViewport(id, element) {
477
+ const callback = (distance) => this.reportViewportDistance(id, distance);
478
+ this.viewportObserver.observe(element, callback);
479
+ return () => this.viewportObserver.unobserve(element);
480
+ }
481
+ /** Called whenever a resource's distance-from-viewport changes. */
482
+ reportViewportDistance(id, distance) {
483
+ const record = this.registry.get(id);
484
+ if (!record) return;
485
+ this.registry.setViewportDistance(id, distance);
486
+ if (record.state === "idle") {
487
+ const withinZone = this.priorityEngine.isWithinPreloadZone(distance, this.preloadDistance);
488
+ if (withinZone) {
489
+ this.registry.setState(id, "eligible");
490
+ if (distance > 0) {
491
+ this.scheduler.prefetch(id, `entered preload zone at ${Math.round(distance)}px`);
492
+ }
493
+ } else {
494
+ this.scheduler.defer(id, `outside preload zone (${Math.round(distance)}px > ${this.preloadDistance}px)`);
495
+ }
496
+ }
497
+ this.requestPass();
498
+ }
499
+ requestPass() {
500
+ this.scheduler.requestPass({
501
+ connection: this.connection,
502
+ preloadDistance: this.preloadDistance
503
+ });
504
+ }
505
+ getConnection() {
506
+ return this.connection;
507
+ }
508
+ getDebugSnapshot() {
509
+ return {
510
+ resources: this.registry.all(),
511
+ decisions: this.scheduler.getDecisionLog(),
512
+ connection: this.connection
513
+ };
514
+ }
515
+ setPriority(id, priority) {
516
+ this.registry.update(id, { priority });
517
+ this.requestPass();
518
+ }
519
+ destroy() {
520
+ var _a;
521
+ (_a = this.unsubscribeConnection) == null ? void 0 : _a.call(this);
522
+ this.viewportObserver.disconnect();
523
+ this.refCounts.clear();
524
+ this.registry.clear();
525
+ }
526
+ logDecision(decision) {
527
+ if (typeof console === "undefined") return;
528
+ console.debug(
529
+ `[react-fastload] ${decision.action} "${decision.resourceId}" \u2014 ${decision.reason}`
530
+ );
531
+ }
532
+ };
533
+
534
+ // src/observers/PerformanceObserver.ts
535
+ var BrowserPerformanceObserver = class {
536
+ constructor() {
537
+ this.observers = [];
538
+ }
539
+ observe(entryTypes, callback, options) {
540
+ var _a;
541
+ if (!supportsPerformanceObserver()) return;
542
+ for (const type of entryTypes) {
543
+ try {
544
+ const observer = new PerformanceObserver((list) => {
545
+ callback(list.getEntries());
546
+ });
547
+ observer.observe({ type, buffered: (_a = options == null ? void 0 : options.buffered) != null ? _a : true });
548
+ this.observers.push(observer);
549
+ } catch {
550
+ }
551
+ }
552
+ }
553
+ disconnect() {
554
+ for (const observer of this.observers) observer.disconnect();
555
+ this.observers = [];
556
+ }
557
+ };
558
+
559
+ // src/metrics/WebVitals.ts
560
+ var WebVitals = class {
561
+ constructor() {
562
+ this.snapshot = { fcp: null, lcp: null, cls: null };
563
+ this.listeners = /* @__PURE__ */ new Set();
564
+ this.perfObserver = new BrowserPerformanceObserver();
565
+ this.clsAccumulator = 0;
566
+ }
567
+ start() {
568
+ if (!supportsNavigationTiming()) return;
569
+ this.perfObserver.observe(["paint"], (entries) => {
570
+ for (const entry of entries) {
571
+ if (entry.name === "first-contentful-paint") {
572
+ this.update({ fcp: entry.startTime });
573
+ }
574
+ }
575
+ });
576
+ this.perfObserver.observe(["largest-contentful-paint"], (entries) => {
577
+ const last = entries[entries.length - 1];
578
+ if (last) {
579
+ const value = last.renderTime || last.loadTime || last.startTime;
580
+ this.update({ lcp: value });
581
+ }
582
+ });
583
+ this.perfObserver.observe(["layout-shift"], (entries) => {
584
+ for (const entry of entries) {
585
+ if (!entry.hadRecentInput) {
586
+ this.clsAccumulator += entry.value;
587
+ }
588
+ }
589
+ this.update({ cls: this.clsAccumulator });
590
+ });
591
+ }
592
+ getSnapshot() {
593
+ return { ...this.snapshot };
594
+ }
595
+ subscribe(listener) {
596
+ this.listeners.add(listener);
597
+ return () => this.listeners.delete(listener);
598
+ }
599
+ stop() {
600
+ this.perfObserver.disconnect();
601
+ this.listeners.clear();
602
+ }
603
+ update(patch) {
604
+ this.snapshot = { ...this.snapshot, ...patch };
605
+ for (const listener of this.listeners) listener(this.snapshot);
606
+ }
607
+ };
608
+
609
+ // src/metrics/ResourceMetrics.ts
610
+ function getNavigationMetrics() {
611
+ if (!supportsNavigationTiming()) {
612
+ return { ttfb: null, domContentLoaded: null, loadEvent: null };
613
+ }
614
+ const [nav] = performance.getEntriesByType("navigation");
615
+ if (!nav) {
616
+ return { ttfb: null, domContentLoaded: null, loadEvent: null };
617
+ }
618
+ return {
619
+ ttfb: round(nav.responseStart - nav.startTime),
620
+ domContentLoaded: round(nav.domContentLoadedEventEnd - nav.startTime),
621
+ loadEvent: nav.loadEventEnd > 0 ? round(nav.loadEventEnd - nav.startTime) : null
622
+ };
623
+ }
624
+ function getResourceTimingSummary() {
625
+ if (!supportsNavigationTiming()) {
626
+ return { totalResources: 0, totalTransferBytes: 0, slowestResourceMs: null };
627
+ }
628
+ const entries = performance.getEntriesByType("resource");
629
+ let totalTransferBytes = 0;
630
+ let slowest = null;
631
+ for (const entry of entries) {
632
+ totalTransferBytes += entry.transferSize || 0;
633
+ const duration = entry.duration;
634
+ if (slowest === null || duration > slowest) slowest = duration;
635
+ }
636
+ return {
637
+ totalResources: entries.length,
638
+ totalTransferBytes,
639
+ slowestResourceMs: slowest !== null ? round(slowest) : null
640
+ };
641
+ }
642
+ function round(value) {
643
+ return Math.round(value * 100) / 100;
644
+ }
645
+
646
+ // src/metrics/MetricsCollector.ts
647
+ var MetricsCollector = class {
648
+ constructor(loadManager) {
649
+ this.loadManager = loadManager;
650
+ this.webVitals = new WebVitals();
651
+ this.started = false;
652
+ }
653
+ start() {
654
+ if (this.started) return;
655
+ this.started = true;
656
+ this.webVitals.start();
657
+ }
658
+ stop() {
659
+ this.webVitals.stop();
660
+ this.started = false;
661
+ }
662
+ subscribeWebVitals(listener) {
663
+ return this.webVitals.subscribe(listener);
664
+ }
665
+ snapshot() {
666
+ const vitals = this.webVitals.getSnapshot();
667
+ const nav = getNavigationMetrics();
668
+ const resourceTiming = getResourceTimingSummary();
669
+ const registrySummary = this.loadManager.registry.summary();
670
+ return {
671
+ fcp: vitals.fcp,
672
+ lcp: vitals.lcp,
673
+ cls: vitals.cls,
674
+ ttfb: nav.ttfb,
675
+ totalResourcesObserved: resourceTiming.totalResources,
676
+ totalTransferBytesObserved: resourceTiming.totalTransferBytes,
677
+ initialRequests: registrySummary.initialRequests,
678
+ deferredRequests: registrySummary.deferredRequests,
679
+ deferredResources: registrySummary.deferredResources,
680
+ bytesDeferred: registrySummary.bytesDeferred,
681
+ imagesDeferred: registrySummary.imagesDeferred,
682
+ videosDeferred: registrySummary.videosDeferred,
683
+ audiosDeferred: registrySummary.audiosDeferred,
684
+ componentsDeferred: registrySummary.componentsDeferred
685
+ };
686
+ }
687
+ };
688
+ var FastLoadContext = React5.createContext(null);
689
+ var STRATEGY_CONCURRENCY = {
690
+ adaptive: 4,
691
+ eager: 8,
692
+ conservative: 2
693
+ };
694
+ function FastLoadProvider({
695
+ children,
696
+ strategy = "adaptive",
697
+ preloadDistance = 1e3,
698
+ concurrency,
699
+ debug = false
700
+ }) {
701
+ const loadManagerRef = React5.useRef();
702
+ if (!loadManagerRef.current) {
703
+ loadManagerRef.current = new LoadManager({
704
+ preloadDistance,
705
+ concurrency: concurrency != null ? concurrency : STRATEGY_CONCURRENCY[strategy],
706
+ debug
707
+ });
708
+ }
709
+ const metricsCollectorRef = React5.useRef();
710
+ if (!metricsCollectorRef.current) {
711
+ metricsCollectorRef.current = new MetricsCollector(loadManagerRef.current);
712
+ }
713
+ React5.useEffect(() => {
714
+ const manager = loadManagerRef.current;
715
+ const collector = metricsCollectorRef.current;
716
+ collector.start();
717
+ return () => {
718
+ collector.stop();
719
+ manager.destroy();
720
+ };
721
+ }, []);
722
+ const value = React5.useMemo(
723
+ () => ({
724
+ loadManager: loadManagerRef.current,
725
+ metricsCollector: metricsCollectorRef.current,
726
+ debug
727
+ }),
728
+ [debug]
729
+ );
730
+ return /* @__PURE__ */ jsxRuntime.jsxs(FastLoadContext.Provider, { value, children: [
731
+ children,
732
+ debug && /* @__PURE__ */ jsxRuntime.jsx(FastLoadDebugPanel, {})
733
+ ] });
734
+ }
735
+ function useFastLoadContext() {
736
+ const ctx = React5.useContext(FastLoadContext);
737
+ if (!ctx) {
738
+ throw new Error(
739
+ "react-fastload: this hook/component must be used inside a <FastLoadProvider>."
740
+ );
741
+ }
742
+ return ctx;
743
+ }
744
+ function FastLoadDebugPanel() {
745
+ var _a, _b;
746
+ const { loadManager } = useFastLoadContext();
747
+ const [snapshot, setSnapshot] = React5.useState(() => loadManager.getDebugSnapshot());
748
+ React5.useEffect(() => {
749
+ const unsubscribe = loadManager.registry.subscribe(() => {
750
+ setSnapshot(loadManager.getDebugSnapshot());
751
+ });
752
+ const interval = setInterval(() => setSnapshot(loadManager.getDebugSnapshot()), 1e3);
753
+ return () => {
754
+ unsubscribe();
755
+ clearInterval(interval);
756
+ };
757
+ }, [loadManager]);
758
+ const isProduction = typeof process !== "undefined" && ((_a = process.env) == null ? void 0 : _a.NODE_ENV) === "production";
759
+ if (isProduction) return null;
760
+ return /* @__PURE__ */ jsxRuntime.jsxs(
761
+ "div",
762
+ {
763
+ style: {
764
+ position: "fixed",
765
+ bottom: 8,
766
+ right: 8,
767
+ zIndex: 999999,
768
+ maxWidth: 360,
769
+ maxHeight: 320,
770
+ overflow: "auto",
771
+ background: "rgba(17, 17, 17, 0.92)",
772
+ color: "#e6e6e6",
773
+ fontFamily: "monospace",
774
+ fontSize: 11,
775
+ borderRadius: 8,
776
+ padding: 10,
777
+ lineHeight: 1.4
778
+ },
779
+ "data-testid": "fastload-debug-panel",
780
+ children: [
781
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontWeight: 700, marginBottom: 6 }, children: "react-fastload debug" }),
782
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
783
+ "connection: ",
784
+ (_b = snapshot.connection.effectiveType) != null ? _b : "unknown",
785
+ " (saveData: ",
786
+ String(snapshot.connection.saveData),
787
+ ")"
788
+ ] }),
789
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: 6 }, children: [
790
+ "resources (",
791
+ snapshot.resources.length,
792
+ ")"
793
+ ] }),
794
+ snapshot.resources.map((r) => /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { opacity: r.state === "loaded" ? 0.6 : 1 }, children: [
795
+ "[",
796
+ r.priority,
797
+ "] ",
798
+ r.id,
799
+ " \u2014 ",
800
+ r.state,
801
+ r.deferred ? " (deferred)" : "",
802
+ r.prefetched ? " (prefetched)" : ""
803
+ ] }, r.id)),
804
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: { marginTop: 6 }, children: "recent decisions" }),
805
+ snapshot.decisions.slice(-8).map((d, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
806
+ d.action,
807
+ " ",
808
+ d.resourceId,
809
+ ": ",
810
+ d.reason
811
+ ] }, i))
812
+ ]
813
+ }
814
+ );
815
+ }
816
+ function useLazyLoad(options) {
817
+ const { loadManager } = useFastLoadContext();
818
+ const { id, type, priority, strategy, estimatedSize, meta, loader } = options;
819
+ const unobserveRef = React5.useRef(void 0);
820
+ const elementRef = React5.useRef(null);
821
+ const shouldObserveRef = React5.useRef(strategy !== "eager" && priority !== "CRITICAL");
822
+ shouldObserveRef.current = strategy !== "eager" && priority !== "CRITICAL";
823
+ const [state, setState] = React5.useState("idle");
824
+ const [record, setRecord] = React5.useState(void 0);
825
+ React5.useEffect(() => {
826
+ const initial = loadManager.register({ id, type, priority, strategy, estimatedSize, meta });
827
+ setRecord(initial);
828
+ setState(initial.state);
829
+ loadManager.setLoader(id, loader);
830
+ const unsubscribe = loadManager.registry.subscribe((all) => {
831
+ const found = all.find((r) => r.id === id);
832
+ if (found) {
833
+ setRecord(found);
834
+ setState(found.state);
835
+ }
836
+ });
837
+ if (shouldObserveRef.current && elementRef.current) {
838
+ unobserveRef.current = loadManager.observeViewport(id, elementRef.current);
839
+ }
840
+ return () => {
841
+ var _a;
842
+ unsubscribe();
843
+ (_a = unobserveRef.current) == null ? void 0 : _a.call(unobserveRef);
844
+ unobserveRef.current = void 0;
845
+ loadManager.unregister(id);
846
+ };
847
+ }, [id]);
848
+ const ref = (node) => {
849
+ var _a;
850
+ if (elementRef.current === node) return;
851
+ (_a = unobserveRef.current) == null ? void 0 : _a.call(unobserveRef);
852
+ unobserveRef.current = void 0;
853
+ elementRef.current = node;
854
+ if (node && shouldObserveRef.current) {
855
+ unobserveRef.current = loadManager.observeViewport(id, node);
856
+ }
857
+ };
858
+ return { ref, state, record };
859
+ }
860
+
861
+ // src/resources/ImageLoader.ts
862
+ var PRIORITY_TO_FETCH_PRIORITY = {
863
+ CRITICAL: "high",
864
+ HIGH: "high",
865
+ NORMAL: "auto",
866
+ LOW: "low",
867
+ IDLE: "low"
868
+ };
869
+ function loadImage(options) {
870
+ return new Promise((resolve, reject) => {
871
+ var _a, _b, _c;
872
+ if (typeof Image === "undefined") {
873
+ resolve({});
874
+ return;
875
+ }
876
+ if ((_a = options.signal) == null ? void 0 : _a.aborted) {
877
+ reject(new DOMException("Image load aborted before it started", "AbortError"));
878
+ return;
879
+ }
880
+ const img = new Image();
881
+ if (supportsFetchPriority()) {
882
+ img.fetchPriority = PRIORITY_TO_FETCH_PRIORITY[options.priority];
883
+ }
884
+ img.decoding = (_b = options.decoding) != null ? _b : "async";
885
+ const cleanup = () => {
886
+ var _a2;
887
+ img.onload = null;
888
+ img.onerror = null;
889
+ (_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
890
+ };
891
+ const onAbort = () => {
892
+ cleanup();
893
+ img.src = "";
894
+ reject(new DOMException("Image load aborted", "AbortError"));
895
+ };
896
+ img.onload = () => {
897
+ cleanup();
898
+ resolve(img);
899
+ };
900
+ img.onerror = (event) => {
901
+ cleanup();
902
+ reject(event instanceof Event ? new Error("Image failed to load") : event);
903
+ };
904
+ (_c = options.signal) == null ? void 0 : _c.addEventListener("abort", onAbort, { once: true });
905
+ img.src = options.src;
906
+ });
907
+ }
908
+ function resolvePriority(priority) {
909
+ if (!priority || priority === "auto") return "NORMAL";
910
+ return priority;
911
+ }
912
+ function SmartImage({
913
+ src,
914
+ priority = "auto",
915
+ placeholder,
916
+ resourceId,
917
+ strategy = "auto",
918
+ estimatedSize,
919
+ alt,
920
+ decoding = "async",
921
+ style,
922
+ onLoad,
923
+ onError,
924
+ ...rest
925
+ }) {
926
+ const id = resourceId != null ? resourceId : `image:${src}`;
927
+ const resolvedPriority = resolvePriority(priority);
928
+ const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
929
+ const loader = React5.useMemo(
930
+ () => (resource) => {
931
+ var _a;
932
+ return loadImage({ src, priority: resolvedPriority, decoding, signal: (_a = resource.abortController) == null ? void 0 : _a.signal }).then(
933
+ () => void 0
934
+ );
935
+ },
936
+ // eslint-disable-next-line react-hooks/exhaustive-deps
937
+ [src, resolvedPriority, decoding]
938
+ );
939
+ const { ref, state } = useLazyLoad({
940
+ id,
941
+ type: "image",
942
+ priority: resolvedPriority,
943
+ strategy: effectiveStrategy,
944
+ estimatedSize,
945
+ // Stored so tooling (e.g. a benchmark or devtools panel) can
946
+ // cross-reference a deferred registry entry against its real
947
+ // Resource Timing entry once it eventually loads. Never read by the
948
+ // scheduler itself.
949
+ meta: { src },
950
+ loader
951
+ });
952
+ const shouldRenderRealImage = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
953
+ const containerStyle = {
954
+ ...style,
955
+ ...placeholder && !shouldRenderRealImage ? {
956
+ backgroundImage: `url(${placeholder})`,
957
+ backgroundSize: "cover",
958
+ backgroundPosition: "center"
959
+ } : {}
960
+ };
961
+ if (!shouldRenderRealImage) {
962
+ return /* @__PURE__ */ jsxRuntime.jsx(
963
+ "div",
964
+ {
965
+ ref,
966
+ role: "img",
967
+ "aria-label": alt,
968
+ "aria-busy": state === "eligible",
969
+ "data-fastload-state": state,
970
+ style: { display: "inline-block", ...containerStyle }
971
+ }
972
+ );
973
+ }
974
+ const imgProps = {
975
+ src,
976
+ alt,
977
+ decoding,
978
+ loading: effectiveStrategy === "eager" ? "eager" : "lazy",
979
+ style: containerStyle,
980
+ onLoad,
981
+ onError,
982
+ ...supportsFetchPriority() ? { fetchPriority: fetchPriorityFor(resolvedPriority) } : {},
983
+ ...rest
984
+ };
985
+ return /* @__PURE__ */ jsxRuntime.jsx(
986
+ "img",
987
+ {
988
+ ref,
989
+ "data-fastload-state": state,
990
+ ...imgProps
991
+ }
992
+ );
993
+ }
994
+ function fetchPriorityFor(priority) {
995
+ if (priority === "CRITICAL" || priority === "HIGH") return "high";
996
+ if (priority === "LOW" || priority === "IDLE") return "low";
997
+ return "auto";
998
+ }
999
+
1000
+ // src/resources/VideoLoader.ts
1001
+ function loadVideo(options) {
1002
+ return new Promise((resolve) => {
1003
+ var _a, _b;
1004
+ if ((_a = options.signal) == null ? void 0 : _a.aborted) {
1005
+ resolve();
1006
+ return;
1007
+ }
1008
+ if (options.poster && (options.priority === "CRITICAL" || options.priority === "HIGH")) {
1009
+ if (typeof Image === "undefined") {
1010
+ resolve();
1011
+ return;
1012
+ }
1013
+ const img = new Image();
1014
+ const cleanup = () => {
1015
+ var _a2;
1016
+ img.onload = null;
1017
+ img.onerror = null;
1018
+ (_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
1019
+ };
1020
+ const onAbort = () => {
1021
+ cleanup();
1022
+ img.src = "";
1023
+ resolve();
1024
+ };
1025
+ img.onload = () => {
1026
+ cleanup();
1027
+ resolve();
1028
+ };
1029
+ img.onerror = () => {
1030
+ cleanup();
1031
+ resolve();
1032
+ };
1033
+ (_b = options.signal) == null ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
1034
+ img.src = options.poster;
1035
+ return;
1036
+ }
1037
+ resolve();
1038
+ });
1039
+ }
1040
+ function resolvePriority2(priority) {
1041
+ if (!priority || priority === "auto") return "NORMAL";
1042
+ return priority;
1043
+ }
1044
+ function SmartVideo({
1045
+ src,
1046
+ poster,
1047
+ priority = "auto",
1048
+ resourceId,
1049
+ strategy = "auto",
1050
+ estimatedSize,
1051
+ aspectRatio,
1052
+ autoplay = false,
1053
+ muted,
1054
+ controls = true,
1055
+ style,
1056
+ ...rest
1057
+ }) {
1058
+ const id = resourceId != null ? resourceId : `video:${src}`;
1059
+ const resolvedPriority = resolvePriority2(priority);
1060
+ const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
1061
+ const [userInteracted, setUserInteracted] = React5.useState(false);
1062
+ const loader = React5.useMemo(
1063
+ () => (resource) => {
1064
+ var _a;
1065
+ return loadVideo({ poster, priority: resolvedPriority, signal: (_a = resource.abortController) == null ? void 0 : _a.signal });
1066
+ },
1067
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1068
+ [src, poster, resolvedPriority]
1069
+ );
1070
+ const { ref, state } = useLazyLoad({
1071
+ id,
1072
+ type: "video",
1073
+ priority: resolvedPriority,
1074
+ strategy: effectiveStrategy,
1075
+ estimatedSize,
1076
+ // Stored so tooling (e.g. a benchmark or devtools panel) can
1077
+ // cross-reference a deferred registry entry against its real
1078
+ // Resource Timing entry once it eventually loads. Never read by the
1079
+ // scheduler itself.
1080
+ meta: { src },
1081
+ loader
1082
+ });
1083
+ const isEligible = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
1084
+ const shouldMountVideo = isEligible || userInteracted;
1085
+ if (!shouldMountVideo) {
1086
+ return /* @__PURE__ */ jsxRuntime.jsx(
1087
+ "div",
1088
+ {
1089
+ ref,
1090
+ role: "button",
1091
+ tabIndex: 0,
1092
+ "aria-label": "Load video",
1093
+ "data-fastload-state": state,
1094
+ onClick: () => setUserInteracted(true),
1095
+ onKeyDown: (e) => {
1096
+ if (e.key === "Enter" || e.key === " ") setUserInteracted(true);
1097
+ },
1098
+ style: {
1099
+ position: "relative",
1100
+ cursor: "pointer",
1101
+ backgroundImage: poster ? `url(${poster})` : void 0,
1102
+ backgroundSize: "cover",
1103
+ backgroundPosition: "center",
1104
+ display: "flex",
1105
+ alignItems: "center",
1106
+ justifyContent: "center",
1107
+ minHeight: aspectRatio ? void 0 : 120,
1108
+ aspectRatio,
1109
+ ...style
1110
+ },
1111
+ children: /* @__PURE__ */ jsxRuntime.jsx(PlayGlyph, {})
1112
+ }
1113
+ );
1114
+ }
1115
+ return /* @__PURE__ */ jsxRuntime.jsx(
1116
+ "video",
1117
+ {
1118
+ ref,
1119
+ src,
1120
+ poster,
1121
+ preload: resolvedPriority === "CRITICAL" || resolvedPriority === "HIGH" ? "auto" : "metadata",
1122
+ autoPlay: autoplay && isEligible,
1123
+ muted: autoplay ? true : muted,
1124
+ controls,
1125
+ "data-fastload-state": state,
1126
+ style: { aspectRatio, ...style },
1127
+ ...rest
1128
+ }
1129
+ );
1130
+ }
1131
+ function PlayGlyph() {
1132
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "48", height: "48", viewBox: "0 0 48 48", "aria-hidden": "true", children: [
1133
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "24", cy: "24", r: "23", fill: "rgba(0,0,0,0.55)", stroke: "white", strokeWidth: "1" }),
1134
+ /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "19,14 19,34 35,24", fill: "white" })
1135
+ ] });
1136
+ }
1137
+
1138
+ // src/resources/AudioLoader.ts
1139
+ function loadAudio(options) {
1140
+ return new Promise((resolve) => {
1141
+ var _a, _b;
1142
+ if ((_a = options.signal) == null ? void 0 : _a.aborted) {
1143
+ resolve();
1144
+ return;
1145
+ }
1146
+ if (options.priority !== "CRITICAL" && options.priority !== "HIGH") {
1147
+ resolve();
1148
+ return;
1149
+ }
1150
+ if (typeof Audio === "undefined") {
1151
+ resolve();
1152
+ return;
1153
+ }
1154
+ const audio = new Audio();
1155
+ audio.preload = "metadata";
1156
+ const cleanup = () => {
1157
+ var _a2;
1158
+ audio.removeEventListener("loadedmetadata", finish);
1159
+ audio.removeEventListener("error", finish);
1160
+ (_a2 = options.signal) == null ? void 0 : _a2.removeEventListener("abort", onAbort);
1161
+ };
1162
+ const finish = () => {
1163
+ cleanup();
1164
+ resolve();
1165
+ };
1166
+ const onAbort = () => {
1167
+ cleanup();
1168
+ audio.src = "";
1169
+ resolve();
1170
+ };
1171
+ audio.addEventListener("loadedmetadata", finish, { once: true });
1172
+ audio.addEventListener("error", finish, { once: true });
1173
+ (_b = options.signal) == null ? void 0 : _b.addEventListener("abort", onAbort, { once: true });
1174
+ audio.src = options.src;
1175
+ });
1176
+ }
1177
+ function resolvePriority3(priority) {
1178
+ if (!priority || priority === "auto") return "NORMAL";
1179
+ return priority;
1180
+ }
1181
+ function SmartAudio({
1182
+ src,
1183
+ priority = "auto",
1184
+ resourceId,
1185
+ strategy = "auto",
1186
+ estimatedSize,
1187
+ label,
1188
+ autoplay = false,
1189
+ muted,
1190
+ controls = true,
1191
+ ...rest
1192
+ }) {
1193
+ const id = resourceId != null ? resourceId : `audio:${src}`;
1194
+ const resolvedPriority = resolvePriority3(priority);
1195
+ const effectiveStrategy = strategy === "auto" ? resolvedPriority === "CRITICAL" ? "eager" : "lazy" : strategy;
1196
+ const [userInteracted, setUserInteracted] = React5.useState(false);
1197
+ const loader = React5.useMemo(
1198
+ () => (resource) => {
1199
+ var _a;
1200
+ return loadAudio({ src, priority: resolvedPriority, signal: (_a = resource.abortController) == null ? void 0 : _a.signal });
1201
+ },
1202
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1203
+ [src, resolvedPriority]
1204
+ );
1205
+ const { ref, state } = useLazyLoad({
1206
+ id,
1207
+ type: "audio",
1208
+ priority: resolvedPriority,
1209
+ strategy: effectiveStrategy,
1210
+ estimatedSize,
1211
+ // Stored so tooling (e.g. a benchmark or devtools panel) can
1212
+ // cross-reference a deferred registry entry against its real
1213
+ // Resource Timing entry once it eventually loads. Never read by the
1214
+ // scheduler itself.
1215
+ meta: { src },
1216
+ loader
1217
+ });
1218
+ const isEligible = effectiveStrategy === "eager" || state === "loading" || state === "loaded";
1219
+ const shouldMountAudio = isEligible || userInteracted;
1220
+ if (!shouldMountAudio) {
1221
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1222
+ "div",
1223
+ {
1224
+ ref,
1225
+ role: "button",
1226
+ tabIndex: 0,
1227
+ "aria-label": label ? `Load audio: ${label}` : "Load audio",
1228
+ "data-fastload-state": state,
1229
+ onClick: () => setUserInteracted(true),
1230
+ onKeyDown: (e) => {
1231
+ if (e.key === "Enter" || e.key === " ") setUserInteracted(true);
1232
+ },
1233
+ style: {
1234
+ display: "inline-flex",
1235
+ alignItems: "center",
1236
+ gap: 8,
1237
+ padding: "8px 12px",
1238
+ borderRadius: 999,
1239
+ border: "1px solid rgba(0,0,0,0.15)",
1240
+ cursor: "pointer",
1241
+ userSelect: "none"
1242
+ },
1243
+ children: [
1244
+ /* @__PURE__ */ jsxRuntime.jsx(PlayGlyph2, {}),
1245
+ label != null ? label : "Load audio"
1246
+ ]
1247
+ }
1248
+ );
1249
+ }
1250
+ return /* @__PURE__ */ jsxRuntime.jsx(
1251
+ "audio",
1252
+ {
1253
+ ref,
1254
+ src,
1255
+ preload: resolvedPriority === "CRITICAL" || resolvedPriority === "HIGH" ? "auto" : "metadata",
1256
+ autoPlay: autoplay && isEligible,
1257
+ muted: autoplay ? true : muted,
1258
+ controls,
1259
+ "data-fastload-state": state,
1260
+ ...rest
1261
+ }
1262
+ );
1263
+ }
1264
+ function PlayGlyph2() {
1265
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "6,4 6,20 20,12", fill: "currentColor" }) });
1266
+ }
1267
+
1268
+ // src/resources/ComponentLoader.ts
1269
+ function createComponentLoader(importFn) {
1270
+ let cached;
1271
+ return () => {
1272
+ if (!cached) {
1273
+ cached = importFn();
1274
+ }
1275
+ return cached;
1276
+ };
1277
+ }
1278
+ function lazyComponent(importFn, options = {}) {
1279
+ var _a, _b, _c;
1280
+ const cachedImport = createComponentLoader(importFn);
1281
+ const priority = (_a = options.priority) != null ? _a : "NORMAL";
1282
+ const strategy = (_b = options.strategy) != null ? _b : priority === "CRITICAL" ? "eager" : "lazy";
1283
+ function LazyComponentWrapper(props) {
1284
+ var _a2, _b2, _c2, _d;
1285
+ const generatedId = React5.useId();
1286
+ const id = (_a2 = options.resourceId) != null ? _a2 : `component:${generatedId}`;
1287
+ const LazyReal = React5.useMemo(() => React5__default.default.lazy(cachedImport), []);
1288
+ const loader = React5.useMemo(
1289
+ () => () => cachedImport().then(() => void 0),
1290
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1291
+ []
1292
+ );
1293
+ const { ref, state } = useLazyLoad({
1294
+ id,
1295
+ type: "component",
1296
+ priority,
1297
+ strategy,
1298
+ estimatedSize: options.estimatedSize,
1299
+ loader
1300
+ });
1301
+ const isEligible = strategy === "eager" || state === "eligible" || state === "loading" || state === "loaded";
1302
+ if (!isEligible) {
1303
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { ref, "data-fastload-state": state, children: (_b2 = options.placeholder) != null ? _b2 : null });
1304
+ }
1305
+ const Rendered = LazyReal;
1306
+ return /* @__PURE__ */ jsxRuntime.jsx(React5.Suspense, { fallback: (_d = (_c2 = options.fallback) != null ? _c2 : options.placeholder) != null ? _d : null, children: /* @__PURE__ */ jsxRuntime.jsx(Rendered, { ...props }) });
1307
+ }
1308
+ LazyComponentWrapper.displayName = `LazyComponent(${(_c = options.resourceId) != null ? _c : "anonymous"})`;
1309
+ return LazyComponentWrapper;
1310
+ }
1311
+ function usePriority(resourceId, priority) {
1312
+ const { loadManager } = useFastLoadContext();
1313
+ React5.useEffect(() => {
1314
+ if (!loadManager.registry.has(resourceId)) return;
1315
+ loadManager.setPriority(resourceId, priority);
1316
+ }, [loadManager, resourceId, priority]);
1317
+ }
1318
+ function useFastLoadMetrics() {
1319
+ const { loadManager, metricsCollector } = useFastLoadContext();
1320
+ const [metrics, setMetrics] = React5.useState(() => metricsCollector.snapshot());
1321
+ React5.useEffect(() => {
1322
+ const refresh = () => setMetrics(metricsCollector.snapshot());
1323
+ const unsubVitals = metricsCollector.subscribeWebVitals(refresh);
1324
+ const unsubRegistry = loadManager.registry.subscribe(refresh);
1325
+ refresh();
1326
+ return () => {
1327
+ unsubVitals();
1328
+ unsubRegistry();
1329
+ };
1330
+ }, [loadManager, metricsCollector]);
1331
+ return metrics;
1332
+ }
1333
+
1334
+ exports.FastLoadProvider = FastLoadProvider;
1335
+ exports.LoadManager = LoadManager;
1336
+ exports.PriorityEngine = PriorityEngine;
1337
+ exports.ResourceRegistry = ResourceRegistry;
1338
+ exports.Scheduler = Scheduler;
1339
+ exports.SmartAudio = SmartAudio;
1340
+ exports.SmartImage = SmartImage;
1341
+ exports.SmartVideo = SmartVideo;
1342
+ exports.lazyComponent = lazyComponent;
1343
+ exports.useFastLoadContext = useFastLoadContext;
1344
+ exports.useFastLoadMetrics = useFastLoadMetrics;
1345
+ exports.useLazyLoad = useLazyLoad;
1346
+ exports.usePriority = usePriority;
1347
+ //# sourceMappingURL=index.cjs.map
1348
+ //# sourceMappingURL=index.cjs.map