bosbun 0.0.4 → 0.0.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosbun",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "A minimalist fullstack framework — SSR + Svelte 5 Runes + Bun + ElysiaJS",
6
6
  "keywords": [
@@ -2,6 +2,7 @@
2
2
  import { router } from "./router.svelte.ts";
3
3
  import { findMatch } from "../matcher.ts";
4
4
  import { clientRoutes } from "bunia:routes";
5
+ import { consumePrefetch, prefetchCache } from "./prefetch.ts";
5
6
 
6
7
  let {
7
8
  ssrMode = false,
@@ -52,9 +53,13 @@
52
53
 
53
54
  // Load components + data in parallel, then update state atomically
54
55
  // to avoid a flash of stale/empty data before the fetch completes.
55
- const dataFetch = match.route.hasServerData
56
- ? fetch(`/__bunia/data?path=${encodeURIComponent(path)}`).then(r => r.json()).catch(() => null)
57
- : Promise.resolve(null);
56
+ const cached = match.route.hasServerData ? consumePrefetch(path) : null;
57
+ prefetchCache.clear(); // clear remaining entries on navigation — matches SvelteKit behavior
58
+ const dataFetch = cached
59
+ ? Promise.resolve(cached)
60
+ : match.route.hasServerData
61
+ ? fetch(`/__bunia/data?path=${encodeURIComponent(path)}`).then(r => r.json()).catch(() => null)
62
+ : Promise.resolve(null);
58
63
 
59
64
  Promise.all([
60
65
  match.route.page(),
@@ -1,6 +1,7 @@
1
1
  import { hydrate } from "svelte";
2
2
  import App from "./App.svelte";
3
3
  import { router } from "./router.svelte.ts";
4
+ import { initPrefetch } from "./prefetch.ts";
4
5
  import { findMatch } from "../matcher.ts";
5
6
  import { clientRoutes } from "bunia:routes";
6
7
 
@@ -11,6 +12,7 @@ async function main() {
11
12
 
12
13
  router.init();
13
14
  router.currentRoute = path;
15
+ initPrefetch();
14
16
 
15
17
  // Resolve the current route so we can pre-load the components
16
18
  // before handing off to App.svelte (avoids a flash of "Loading...")
@@ -0,0 +1,109 @@
1
+ // ─── Link Prefetching ─────────────────────────────────────
2
+ // Supports `data-bunia-preload="hover"` and `data-bunia-preload="viewport"`
3
+ // on <a> elements or their ancestors.
4
+
5
+ export const prefetchCache = new Map<string, any>();
6
+
7
+ // In-flight fetch deduplication
8
+ const pending = new Set<string>();
9
+
10
+ /** Returns cached prefetch data for a path and removes it from cache. */
11
+ export function consumePrefetch(path: string): any | null {
12
+ const data = prefetchCache.get(path);
13
+ if (data === undefined) return null;
14
+ prefetchCache.delete(path);
15
+ return data;
16
+ }
17
+
18
+ /** Prefetches data for a path and stores in cache. No-op if already cached/in-flight. */
19
+ export async function prefetchPath(path: string): Promise<void> {
20
+ if (prefetchCache.has(path)) return;
21
+ if (pending.has(path)) return;
22
+
23
+ pending.add(path);
24
+ try {
25
+ const res = await fetch(`/__bunia/data?path=${encodeURIComponent(path)}`);
26
+ if (res.ok) {
27
+ prefetchCache.set(path, await res.json());
28
+ }
29
+ } catch {
30
+ // Silently ignore — prefetch is best-effort
31
+ } finally {
32
+ pending.delete(path);
33
+ }
34
+ }
35
+
36
+ function getLinkHref(anchor: HTMLAnchorElement): string | null {
37
+ if (anchor.origin !== window.location.origin) return null;
38
+ if (anchor.target) return null;
39
+ if (anchor.hasAttribute("download")) return null;
40
+ return anchor.pathname + anchor.search;
41
+ }
42
+
43
+ function observeViewportLinks(container: Element | Document = document) {
44
+ const observer = new IntersectionObserver((entries) => {
45
+ for (const entry of entries) {
46
+ if (!entry.isIntersecting) continue;
47
+ const anchor = entry.target as HTMLAnchorElement;
48
+ const href = getLinkHref(anchor);
49
+ if (href) prefetchPath(href);
50
+ observer.unobserve(anchor);
51
+ }
52
+ }, { rootMargin: "0px" });
53
+
54
+ const links = (container === document ? document : container as Element)
55
+ .querySelectorAll<HTMLAnchorElement>("a[data-bunia-preload='viewport']");
56
+
57
+ for (const link of links) {
58
+ observer.observe(link);
59
+ }
60
+
61
+ return observer;
62
+ }
63
+
64
+ export function initPrefetch(): void {
65
+ // ── Hover strategy (event delegation, 20ms debounce) ─────
66
+ let hoverTimer: ReturnType<typeof setTimeout> | null = null;
67
+
68
+ document.addEventListener("mouseover", (e) => {
69
+ if (!(e.target instanceof Element)) return;
70
+ // Early exit: skip if no [data-bunia-preload="hover"] ancestor exists
71
+ const preloadEl = e.target.closest("[data-bunia-preload]");
72
+ if (!preloadEl || preloadEl.getAttribute("data-bunia-preload") !== "hover") return;
73
+ const anchor = e.target.closest("a") as HTMLAnchorElement | null;
74
+ if (!anchor) return;
75
+ const href = getLinkHref(anchor);
76
+ if (!href) return;
77
+
78
+ if (hoverTimer) clearTimeout(hoverTimer);
79
+ hoverTimer = setTimeout(() => prefetchPath(href), 100);
80
+ });
81
+
82
+ document.addEventListener("mouseout", () => {
83
+ if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; }
84
+ });
85
+
86
+ // ── Viewport strategy ─────────────────────────────────────
87
+ const observer = observeViewportLinks();
88
+
89
+ // Pick up links added after initial render (e.g., after client navigation)
90
+ const mutation = new MutationObserver((records) => {
91
+ for (const record of records) {
92
+ for (const node of record.addedNodes) {
93
+ if (!(node instanceof Element)) continue;
94
+ // The node itself might be a viewport link
95
+ if (node.matches("a[data-bunia-preload='viewport']")) {
96
+ observer.observe(node as HTMLAnchorElement);
97
+ }
98
+ // Or it might contain viewport links
99
+ for (const link of node.querySelectorAll<HTMLAnchorElement>(
100
+ "a[data-bunia-preload='viewport']"
101
+ )) {
102
+ observer.observe(link);
103
+ }
104
+ }
105
+ }
106
+ });
107
+
108
+ mutation.observe(document.body, { childList: true, subtree: true });
109
+ }
package/src/core/html.ts CHANGED
@@ -98,7 +98,7 @@ export function buildHtml(
98
98
  ${cssLinks}
99
99
  <link rel="stylesheet" href="/bunia-tw.css${cacheBust}">
100
100
  </head>
101
- <body>
101
+ <body data-bunia-preload="hover">
102
102
  <div id="app">${body}</div>${scripts}
103
103
  </body>
104
104
  </html>`;
@@ -116,6 +116,7 @@ export function buildHtmlShell(): string {
116
116
  return _shell;
117
117
  }
118
118
 
119
+
119
120
  let _shellOpen: string | null = null;
120
121
 
121
122
  /** Chunk 1: everything from <!DOCTYPE> through CSS/modulepreload links (head still open) */
@@ -159,7 +160,7 @@ export function buildMetadataChunk(metadata: Metadata | null): string {
159
160
  } else {
160
161
  out += ` <title>Bunia App</title>\n`;
161
162
  }
162
- out += `</head>\n<body>\n${SPINNER}`;
163
+ out += `</head>\n<body data-bunia-preload="hover">\n${SPINNER}`;
163
164
  return out;
164
165
  }
165
166