Feat: Web loader
This commit is contained in:
9
Loader/scripts/config.js
Normal file
9
Loader/scripts/config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Path: Loader/scripts/config.js
|
||||
|
||||
export const LOADER_INTENT_STORAGE_KEY = "moku.loader.intent";
|
||||
export const LOADER_META_STORAGE_KEY = "moku.loader.meta";
|
||||
export const LOADER_SEEN_COOKIE_NAME = "moku_loader_seen";
|
||||
export const LOADER_DEFAULT_INTENT = "/";
|
||||
export const LOADER_ROOT_PREFIX = "/__loader";
|
||||
export const LOADER_HANDOFF_DELAY_MS = 720;
|
||||
export const LOADER_SEEN_COOKIE_MAX_AGE_SECONDS = 1800;
|
||||
25
Loader/scripts/dom.js
Normal file
25
Loader/scripts/dom.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// Path: Loader/scripts/dom.js
|
||||
|
||||
export const getStatusElement = () => {
|
||||
return document.getElementById("loader-status");
|
||||
};
|
||||
|
||||
export const getIntentElement = () => {
|
||||
return document.getElementById("loader-intent");
|
||||
};
|
||||
|
||||
export const setStatus = (value) => {
|
||||
const statusElement = getStatusElement();
|
||||
|
||||
if (statusElement instanceof HTMLElement) {
|
||||
statusElement.textContent = value;
|
||||
}
|
||||
};
|
||||
|
||||
export const setIntentPreview = (value) => {
|
||||
const intentElement = getIntentElement();
|
||||
|
||||
if (intentElement instanceof HTMLElement) {
|
||||
intentElement.textContent = value;
|
||||
}
|
||||
};
|
||||
13
Loader/scripts/handoff.js
Normal file
13
Loader/scripts/handoff.js
Normal file
@@ -0,0 +1,13 @@
|
||||
// Path: Loader/scripts/handoff.js
|
||||
|
||||
import { LOADER_HANDOFF_DELAY_MS } from "./config.js";
|
||||
|
||||
export const scheduleHandoff = (target, onBeforeHandoff) => {
|
||||
window.setTimeout(() => {
|
||||
if (typeof onBeforeHandoff === "function") {
|
||||
onBeforeHandoff();
|
||||
}
|
||||
|
||||
window.location.assign(target);
|
||||
}, LOADER_HANDOFF_DELAY_MS);
|
||||
};
|
||||
19
Loader/scripts/hints.js
Normal file
19
Loader/scripts/hints.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// Path: Loader/scripts/hints.js
|
||||
|
||||
export const collectClientHints = () => {
|
||||
const navigatorConnection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
|
||||
return {
|
||||
capturedAt: new Date().toISOString(),
|
||||
language: navigator.language,
|
||||
languages: Array.isArray(navigator.languages) ? navigator.languages : [],
|
||||
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
|
||||
deviceMemory: navigator.deviceMemory ?? null,
|
||||
networkType: navigatorConnection?.effectiveType ?? null,
|
||||
saveData: navigatorConnection?.saveData ?? null,
|
||||
viewport: {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
},
|
||||
};
|
||||
};
|
||||
32
Loader/scripts/intent.js
Normal file
32
Loader/scripts/intent.js
Normal file
@@ -0,0 +1,32 @@
|
||||
// Path: Loader/scripts/intent.js
|
||||
|
||||
import { LOADER_DEFAULT_INTENT, LOADER_ROOT_PREFIX } from "./config.js";
|
||||
|
||||
export const getExplicitIntent = () => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const next = params.get("next");
|
||||
|
||||
if (typeof next === "string" && next.startsWith("/")) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getCurrentPathIntent = () => {
|
||||
const current = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||
|
||||
if (current === "/" || current === LOADER_ROOT_PREFIX || current === `${LOADER_ROOT_PREFIX}/` || current.startsWith(`${LOADER_ROOT_PREFIX}/`)) {
|
||||
return LOADER_DEFAULT_INTENT;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
export const resolveIntent = () => {
|
||||
return getExplicitIntent() ?? getCurrentPathIntent();
|
||||
};
|
||||
|
||||
export const resolveHandoffTarget = (intent) => {
|
||||
return intent || LOADER_DEFAULT_INTENT;
|
||||
};
|
||||
26
Loader/scripts/main.js
Normal file
26
Loader/scripts/main.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Path: Loader/scripts/main.js
|
||||
|
||||
import { setIntentPreview, setStatus } from "./dom.js";
|
||||
import { scheduleHandoff } from "./handoff.js";
|
||||
import { resolveHandoffTarget, resolveIntent } from "./intent.js";
|
||||
import { markLoaderSeen, persistLoaderState } from "./storage.js";
|
||||
|
||||
const bootLoader = () => {
|
||||
const intent = resolveIntent();
|
||||
const handoffTarget = resolveHandoffTarget(intent);
|
||||
|
||||
setStatus("Capturing route intent");
|
||||
setIntentPreview(intent);
|
||||
persistLoaderState(intent);
|
||||
|
||||
window.setTimeout(() => {
|
||||
setStatus("Preparing handoff");
|
||||
}, 180);
|
||||
|
||||
scheduleHandoff(handoffTarget, () => {
|
||||
setStatus("Handing off to application");
|
||||
markLoaderSeen();
|
||||
});
|
||||
};
|
||||
|
||||
bootLoader();
|
||||
24
Loader/scripts/storage.js
Normal file
24
Loader/scripts/storage.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// Path: Loader/scripts/storage.js
|
||||
|
||||
import { LOADER_INTENT_STORAGE_KEY, LOADER_META_STORAGE_KEY, LOADER_SEEN_COOKIE_MAX_AGE_SECONDS, LOADER_SEEN_COOKIE_NAME } from "./config.js";
|
||||
import { collectClientHints } from "./hints.js";
|
||||
|
||||
export const persistLoaderState = (intent) => {
|
||||
const payload = {
|
||||
intent,
|
||||
hints: collectClientHints(),
|
||||
};
|
||||
|
||||
try {
|
||||
window.sessionStorage.setItem(LOADER_INTENT_STORAGE_KEY, intent);
|
||||
window.sessionStorage.setItem(LOADER_META_STORAGE_KEY, JSON.stringify(payload));
|
||||
} catch (error) {
|
||||
console.warn("[Loader] Unable to persist bootstrap state.", error);
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
export const markLoaderSeen = () => {
|
||||
document.cookie = `${LOADER_SEEN_COOKIE_NAME}=1; Path=/; Max-Age=${LOADER_SEEN_COOKIE_MAX_AGE_SECONDS}; SameSite=Lax`;
|
||||
};
|
||||
Reference in New Issue
Block a user