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