veryfront 0.1.850 → 0.1.852

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/esm/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.850",
3
+ "version": "0.1.852",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "minimumDependencyAge": {
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,cA+0B3B,CAAC"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../../../../src/src/html/hydration-script-builder/templates/router.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,cAklC3B,CAAC"}
@@ -36,7 +36,12 @@ export const getRouterScript = () => `
36
36
  const BACKGROUND_REFRESH_INTERVAL_MS = 30 * 1000;
37
37
  const PREFETCH_DELAY_MS = 100;
38
38
  const MAX_PREFETCH_PATHS = 100;
39
+ const IDLE_PREFETCH_DELAY_MS = 1200;
40
+ const IDLE_PREFETCH_MAX_LINKS = 4;
41
+ const VIEWPORT_PREFETCH_MAX_LINKS = 8;
42
+ const VIEWPORT_PREFETCH_ROOT_MARGIN = '200px';
39
43
  const MAX_ROUTE_TIMINGS = 100;
44
+ const MAX_SERVER_TIMING_LENGTH = 1024;
40
45
 
41
46
  // ============================================
42
47
  // Debug logging (production-safe)
@@ -159,6 +164,139 @@ export const getRouterScript = () => `
159
164
  return entry;
160
165
  }
161
166
 
167
+ function sanitizeServerTimingHeader(value) {
168
+ if (!value) return null;
169
+
170
+ const metrics = [];
171
+ const printable = String(value).replace(/[^\\x20-\\x7E]/g, ' ').trim();
172
+ if (!printable) return null;
173
+
174
+ for (const item of printable.split(',')) {
175
+ const segments = item.split(';').map((segment) => segment.trim()).filter(Boolean);
176
+ const name = sanitizeServerTimingMetricName(segments[0]);
177
+ if (!name) continue;
178
+
179
+ for (const segment of segments.slice(1)) {
180
+ const [key, rawValue = ''] = segment.split('=');
181
+ if (key.trim().toLowerCase() !== 'dur') continue;
182
+
183
+ const duration = Number(rawValue.trim().replace(/^"|"$/g, ''));
184
+ if (!Number.isFinite(duration) || duration < 0) continue;
185
+
186
+ metrics.push(name + ';dur=' + (Math.round(duration * 100) / 100).toFixed(2));
187
+ break;
188
+ }
189
+ }
190
+
191
+ const sanitized = metrics.join(', ');
192
+ return sanitized ? sanitized.slice(0, MAX_SERVER_TIMING_LENGTH) : null;
193
+ }
194
+
195
+ function sanitizeServerTimingMetricName(name) {
196
+ return String(name || '').trim().replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 128);
197
+ }
198
+
199
+ function parseServerTimingMetrics(value) {
200
+ const header = sanitizeServerTimingHeader(value);
201
+ if (!header) return null;
202
+
203
+ const metrics = {};
204
+ for (const item of header.split(',')) {
205
+ const segments = item.split(';').map((segment) => segment.trim()).filter(Boolean);
206
+ const name = sanitizeServerTimingMetricName(segments[0]);
207
+ if (!name) continue;
208
+
209
+ for (const segment of segments.slice(1)) {
210
+ const [key, rawValue = ''] = segment.split('=');
211
+ if (key.trim().toLowerCase() !== 'dur') continue;
212
+
213
+ const duration = Number(rawValue.trim().replace(/^"|"$/g, ''));
214
+ if (Number.isFinite(duration) && duration >= 0) {
215
+ metrics[name] = Math.round(duration * 100) / 100;
216
+ }
217
+ }
218
+ }
219
+
220
+ return Object.keys(metrics).length ? metrics : null;
221
+ }
222
+
223
+ function readResponseServerTiming(response) {
224
+ try {
225
+ return sanitizeServerTimingHeader(response.headers?.get('server-timing'));
226
+ } catch (_) {
227
+ return null;
228
+ }
229
+ }
230
+
231
+ function roundRouteTimingValue(value) {
232
+ return Math.round(value * 100) / 100;
233
+ }
234
+
235
+ function extractResourceTiming(entry) {
236
+ const fields = [
237
+ 'startTime',
238
+ 'requestStart',
239
+ 'responseStart',
240
+ 'responseEnd',
241
+ 'duration',
242
+ 'transferSize',
243
+ 'encodedBodySize',
244
+ 'decodedBodySize'
245
+ ];
246
+ const timing = {};
247
+
248
+ for (const field of fields) {
249
+ const value = entry?.[field];
250
+ if (typeof value === 'number' && Number.isFinite(value) && value >= 0) {
251
+ timing[field] = roundRouteTimingValue(value);
252
+ }
253
+ }
254
+
255
+ return Object.keys(timing).length ? timing : null;
256
+ }
257
+
258
+ function getPageDataResourceTiming(endpoint, fetchStartedAt) {
259
+ try {
260
+ if (typeof performance === 'undefined' || typeof performance.getEntriesByName !== 'function') {
261
+ return null;
262
+ }
263
+
264
+ const href = new URL(endpoint, window.location.href).href;
265
+ const entries = performance.getEntriesByName(href, 'resource');
266
+ if (!entries.length) return null;
267
+
268
+ for (let index = entries.length - 1; index >= 0; index--) {
269
+ const entry = entries[index];
270
+ if (
271
+ typeof entry?.responseEnd === 'number' &&
272
+ Number.isFinite(entry.responseEnd) &&
273
+ entry.responseEnd + 1 >= fetchStartedAt
274
+ ) {
275
+ return extractResourceTiming(entry);
276
+ }
277
+ }
278
+
279
+ return null;
280
+ } catch (_) {
281
+ return null;
282
+ }
283
+ }
284
+
285
+ function buildPageDataTimingDetail(response, endpoint, fetchStartedAt, source) {
286
+ const detail = { source, status: response.status };
287
+ const serverTiming = readResponseServerTiming(response);
288
+ if (serverTiming) {
289
+ detail.serverTiming = serverTiming;
290
+ const serverTimingMetrics = parseServerTimingMetrics(serverTiming);
291
+ if (serverTimingMetrics) detail.serverTimingMetrics = serverTimingMetrics;
292
+ }
293
+
294
+ const resourceTiming = getPageDataResourceTiming(response.url || endpoint, fetchStartedAt);
295
+ if (resourceTiming) detail.resourceTiming = resourceTiming;
296
+
297
+ return detail;
298
+ }
299
+
162
300
  // ============================================
163
301
  // LRU Cache with TTL (single Map to prevent sync issues)
164
302
  // ============================================
@@ -320,7 +458,12 @@ export const getRouterScript = () => `
320
458
  if (!response.ok) {
321
459
  perfEnd('fetch:' + path);
322
460
  if (recordRouteTiming) {
323
- emitRouteTiming('page-data', path, startedAt, { source: timingSource, status: response.status });
461
+ emitRouteTiming(
462
+ 'page-data',
463
+ path,
464
+ startedAt,
465
+ buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)
466
+ );
324
467
  }
325
468
  const error = new Error('Failed to fetch page data: ' + response.status);
326
469
  error.status = response.status;
@@ -332,7 +475,12 @@ export const getRouterScript = () => `
332
475
  perfEnd('parse:' + path);
333
476
  perfEnd('fetch:' + path);
334
477
  if (recordRouteTiming) {
335
- emitRouteTiming('page-data', path, startedAt, { source: timingSource, status: response.status });
478
+ emitRouteTiming(
479
+ 'page-data',
480
+ path,
481
+ startedAt,
482
+ buildPageDataTimingDetail(response, endpoint, startedAt, timingSource)
483
+ );
336
484
  }
337
485
 
338
486
  if (triggerReloadOnVersionMismatch) {
@@ -614,6 +762,7 @@ export const getRouterScript = () => `
614
762
  container.__reactRoot.render(tree);
615
763
  perfEnd('render:reactRender');
616
764
  log('Page re-rendered via SPA');
765
+ scheduleRoutePrefetchRefresh();
617
766
  return;
618
767
  }
619
768
 
@@ -629,6 +778,9 @@ export const getRouterScript = () => `
629
778
  // ============================================
630
779
  let prefetchTimeout = null;
631
780
  let currentHoverLink = null;
781
+ let routePrefetchRefreshPending = false;
782
+ let viewportPrefetchObserver = null;
783
+ const observedPrefetchLinks = new WeakSet();
632
784
  const prefetchedPaths = new Set();
633
785
  const inFlightPrefetches = new Set();
634
786
 
@@ -650,6 +802,51 @@ export const getRouterScript = () => `
650
802
  return allPaths;
651
803
  }
652
804
 
805
+ function getCurrentRouteHref() {
806
+ return window.location.pathname + window.location.search;
807
+ }
808
+
809
+ function getInternalRouteHrefFromLink(link) {
810
+ if (
811
+ !link ||
812
+ link.target === '_blank' ||
813
+ link.hasAttribute('download') ||
814
+ link.getAttribute('data-prefetch') === 'false'
815
+ ) {
816
+ return null;
817
+ }
818
+
819
+ const href = link.getAttribute('href');
820
+ if (!href || href.startsWith('#') || href.startsWith('//') || !href.startsWith('/')) return null;
821
+
822
+ try {
823
+ const url = new URL(href, window.location.origin);
824
+ if (url.origin !== window.location.origin) return null;
825
+
826
+ const routeHref = url.pathname + url.search;
827
+ return routeHref === getCurrentRouteHref() ? null : routeHref;
828
+ } catch (_) {
829
+ return null;
830
+ }
831
+ }
832
+
833
+ function getEligiblePrefetchLinks(limit) {
834
+ const links = [];
835
+ const seenHrefs = new Set();
836
+
837
+ for (const link of document.querySelectorAll('a[href]')) {
838
+ const href = getInternalRouteHrefFromLink(link);
839
+ if (!href || seenHrefs.has(href)) continue;
840
+
841
+ seenHrefs.add(href);
842
+ links.push({ link, href });
843
+
844
+ if (links.length >= limit) break;
845
+ }
846
+
847
+ return links;
848
+ }
849
+
653
850
  async function preloadModulesForPageData(pageData, path) {
654
851
  if (!pageData) return;
655
852
  if (pageData.releaseId && window.__veryfrontSetReleaseId) {
@@ -698,6 +895,62 @@ export const getRouterScript = () => `
698
895
  });
699
896
  }
700
897
 
898
+ function prefetchEligibleRouteLinks(limit) {
899
+ for (const { href } of getEligiblePrefetchLinks(limit)) {
900
+ prefetchPage(href);
901
+ }
902
+ }
903
+
904
+ function ensureViewportPrefetchObserver() {
905
+ if (viewportPrefetchObserver || typeof IntersectionObserver !== 'function') {
906
+ return viewportPrefetchObserver;
907
+ }
908
+
909
+ viewportPrefetchObserver = new IntersectionObserver((entries) => {
910
+ for (const entry of entries) {
911
+ if (!entry.isIntersecting) continue;
912
+
913
+ viewportPrefetchObserver?.unobserve(entry.target);
914
+ const href = getInternalRouteHrefFromLink(entry.target);
915
+ if (href) prefetchPage(href);
916
+ }
917
+ }, { rootMargin: VIEWPORT_PREFETCH_ROOT_MARGIN });
918
+
919
+ return viewportPrefetchObserver;
920
+ }
921
+
922
+ function observeViewportPrefetchLinks() {
923
+ const observer = ensureViewportPrefetchObserver();
924
+ if (!observer) return;
925
+
926
+ for (const { link } of getEligiblePrefetchLinks(VIEWPORT_PREFETCH_MAX_LINKS)) {
927
+ if (observedPrefetchLinks.has(link)) continue;
928
+
929
+ observedPrefetchLinks.add(link);
930
+ observer.observe(link);
931
+ }
932
+ }
933
+
934
+ function runRoutePrefetchRefresh() {
935
+ routePrefetchRefreshPending = false;
936
+ prefetchEligibleRouteLinks(IDLE_PREFETCH_MAX_LINKS);
937
+ observeViewportPrefetchLinks();
938
+ }
939
+
940
+ function scheduleRoutePrefetchRefresh() {
941
+ if (routePrefetchRefreshPending) return;
942
+
943
+ routePrefetchRefreshPending = true;
944
+ setTimeout(() => {
945
+ if (typeof requestIdleCallback === 'function') {
946
+ requestIdleCallback(runRoutePrefetchRefresh, { timeout: IDLE_PREFETCH_DELAY_MS });
947
+ return;
948
+ }
949
+
950
+ runRoutePrefetchRefresh();
951
+ }, IDLE_PREFETCH_DELAY_MS);
952
+ }
953
+
701
954
  // ============================================
702
955
  // Router object
703
956
  // ============================================
@@ -802,8 +1055,8 @@ export const getRouterScript = () => `
802
1055
  const link = e.target.closest('a[href]');
803
1056
  if (!link) return;
804
1057
 
805
- const href = link.getAttribute('href');
806
- if (!href?.startsWith('/') || href.startsWith('//')) return;
1058
+ const href = getInternalRouteHrefFromLink(link);
1059
+ if (!href) return;
807
1060
 
808
1061
  if (currentHoverLink === link) return;
809
1062
 
@@ -834,6 +1087,12 @@ export const getRouterScript = () => `
834
1087
  true
835
1088
  );
836
1089
 
1090
+ if (document.readyState === 'loading') {
1091
+ document.addEventListener('DOMContentLoaded', scheduleRoutePrefetchRefresh, { once: true });
1092
+ } else {
1093
+ scheduleRoutePrefetchRefresh();
1094
+ }
1095
+
837
1096
  // ============================================
838
1097
  // Router hooks
839
1098
  // ============================================
@@ -60,7 +60,7 @@ export function useRouter() {
60
60
  }
61
61
  /** Renders an anchor element annotated for Veryfront prefetch handling. */
62
62
  export function Link({ prefetch = true, children, ...rest }) {
63
- return React.createElement("a", { ...rest, "data-prefetch": prefetch ? "true" : undefined }, children);
63
+ return React.createElement("a", { ...rest, "data-prefetch": prefetch ? "true" : "false" }, children);
64
64
  }
65
65
  /** Provides page context to route and MDX descendants. */
66
66
  export function PageContextProvider({ children, pageContext, }) {
@@ -1,3 +1,3 @@
1
1
  /** Shared version value. */
2
- export declare const VERSION = "0.1.850";
2
+ export declare const VERSION = "0.1.852";
3
3
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,4 +1,4 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
3
  /** Shared version value. */
4
- export const VERSION = "0.1.850";
4
+ export const VERSION = "0.1.852";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.850",
3
+ "version": "0.1.852",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",