Separate out into files
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
import { computed, ref, shallowRef } from "vue";
|
||||
import type { RouteLocationNormalizedLoaded } from "vue-router";
|
||||
import type {
|
||||
NativeCacheStats,
|
||||
NativeEvictionReason,
|
||||
NativeTransaction,
|
||||
NativeViewEntry,
|
||||
} from "../types";
|
||||
import { DiagnosticChannel } from "./diagnostics";
|
||||
import { NativeHistoryLedger } from "./history-ledger";
|
||||
import { createViewEntry, diagnosticRoute, monotonicNow } from "./route-entry";
|
||||
|
||||
export class ViewStore {
|
||||
readonly entries = shallowRef<NativeViewEntry[]>([]);
|
||||
readonly activeKey = ref("");
|
||||
readonly cacheStats;
|
||||
|
||||
private totalEvictions = 0;
|
||||
private lastEviction: NativeCacheStats["lastEviction"];
|
||||
|
||||
constructor(
|
||||
private readonly maxInactive: number,
|
||||
private readonly history: NativeHistoryLedger,
|
||||
private readonly diagnostics: DiagnosticChannel,
|
||||
) {
|
||||
this.cacheStats = computed(() => {
|
||||
const entries = this.entries.value;
|
||||
const mounted = entries.filter((entry) => entry.mounted);
|
||||
const inactive = mounted.filter(
|
||||
(entry) => entry.key !== this.activeKey.value,
|
||||
);
|
||||
return {
|
||||
maxInactive: this.maxInactive,
|
||||
descriptors: entries.length,
|
||||
mounted: mounted.length,
|
||||
inactive: inactive.length,
|
||||
pinned: inactive.filter(
|
||||
(entry) => entry.route.meta.native?.cache === "pin",
|
||||
).length,
|
||||
evicted: entries.filter((entry) => !entry.mounted).length,
|
||||
totalEvictions: this.totalEvictions,
|
||||
lastEviction: this.lastEviction,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
initialize(route: RouteLocationNormalizedLoaded) {
|
||||
if (this.entries.value.length) return;
|
||||
const initial = createViewEntry(route, "active");
|
||||
this.entries.value = [initial];
|
||||
this.activeKey.value = initial.key;
|
||||
this.history.initialize(initial.key);
|
||||
}
|
||||
|
||||
active() {
|
||||
return this.byKey(this.activeKey.value);
|
||||
}
|
||||
|
||||
byKey(key: string) {
|
||||
return this.entries.value.find((entry) => entry.key === key);
|
||||
}
|
||||
|
||||
findReusable(fullPath: string) {
|
||||
return [...this.entries.value]
|
||||
.reverse()
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.route.fullPath === fullPath &&
|
||||
entry.key !== this.activeKey.value &&
|
||||
!this.history.contains(entry.key),
|
||||
);
|
||||
}
|
||||
|
||||
findHistoryEntry(fullPath: string) {
|
||||
for (const key of [...this.history.keys.value].reverse()) {
|
||||
const entry = this.byKey(key);
|
||||
if (entry?.route.fullPath === fullPath) return entry;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
appendPreview(route: RouteLocationNormalizedLoaded, synthetic = false) {
|
||||
const entry = createViewEntry(route, "preview", synthetic);
|
||||
this.entries.value = [...this.entries.value, entry];
|
||||
return entry;
|
||||
}
|
||||
|
||||
prepareForward(route: RouteLocationNormalizedLoaded, replace: boolean) {
|
||||
let entry = replace ? this.findHistoryEntry(route.fullPath) : undefined;
|
||||
entry ??= this.findReusable(route.fullPath);
|
||||
if (!entry) return { entry: this.appendPreview(route), needsMount: true };
|
||||
|
||||
const needsMount = !entry.mounted;
|
||||
entry.route = route;
|
||||
entry.mounted = true;
|
||||
entry.status = "preview";
|
||||
entry.synthetic = false;
|
||||
entry.lastUsed = monotonicNow();
|
||||
entry.evictionReason = undefined;
|
||||
this.touch();
|
||||
return { entry, needsMount };
|
||||
}
|
||||
|
||||
revive(entry: NativeViewEntry, route: RouteLocationNormalizedLoaded) {
|
||||
entry.route = route;
|
||||
entry.mounted = true;
|
||||
entry.status = "inactive";
|
||||
entry.evictionReason = undefined;
|
||||
this.touch();
|
||||
}
|
||||
|
||||
commitTarget(entry: NativeViewEntry) {
|
||||
entry.status = "active";
|
||||
entry.synthetic = false;
|
||||
entry.committed = true;
|
||||
entry.lastUsed = monotonicNow();
|
||||
this.activeKey.value = entry.key;
|
||||
}
|
||||
|
||||
acceptRoute(
|
||||
route: RouteLocationNormalizedLoaded,
|
||||
transaction: NativeTransaction | null,
|
||||
) {
|
||||
const previousActiveKey = this.activeKey.value;
|
||||
let target = transaction ? this.byKey(transaction.toKey) : undefined;
|
||||
if (target && target.route.fullPath !== route.fullPath) target = undefined;
|
||||
if (!transaction) target ??= this.findHistoryEntry(route.fullPath);
|
||||
target ??= this.findReusable(route.fullPath);
|
||||
if (!target && this.active()?.route.fullPath === route.fullPath)
|
||||
target = this.active();
|
||||
|
||||
if (!target) {
|
||||
target = createViewEntry(route, "active");
|
||||
const activeIndex = this.entries.value.findIndex(
|
||||
(entry) => entry.key === this.activeKey.value,
|
||||
);
|
||||
const head =
|
||||
activeIndex >= 0
|
||||
? this.entries.value.slice(0, activeIndex + 1)
|
||||
: this.entries.value;
|
||||
this.entries.value = [...head, target];
|
||||
} else {
|
||||
target.route = route;
|
||||
target.presentation ??=
|
||||
route.meta.native?.presentation ?? route.meta.native?.transition;
|
||||
target.mounted = true;
|
||||
target.status = "active";
|
||||
target.committed = true;
|
||||
target.lastUsed = monotonicNow();
|
||||
target.evictionReason = undefined;
|
||||
this.touch();
|
||||
}
|
||||
|
||||
if (target.presentation === "sheet" && target.key !== previousActiveKey)
|
||||
target.underlayKey = previousActiveKey || undefined;
|
||||
this.activeKey.value = target.key;
|
||||
this.history.accept(target, transaction);
|
||||
this.markStatuses(transaction);
|
||||
if (!transaction) this.enforceCache();
|
||||
}
|
||||
|
||||
unload(fullPath: string, transaction: NativeTransaction | null) {
|
||||
let unloaded = 0;
|
||||
for (const entry of this.entries.value) {
|
||||
if (entry.route.fullPath !== fullPath || !entry.mounted) continue;
|
||||
if (entry.key === this.activeKey.value) continue;
|
||||
if (
|
||||
entry.key === transaction?.fromKey ||
|
||||
entry.key === transaction?.toKey
|
||||
)
|
||||
continue;
|
||||
this.evict(entry, "manual");
|
||||
unloaded += 1;
|
||||
}
|
||||
if (unloaded) this.touch();
|
||||
return unloaded;
|
||||
}
|
||||
|
||||
trim(
|
||||
options: { includePinned?: boolean; reason?: NativeEvictionReason } = {},
|
||||
) {
|
||||
const reason = options.reason ?? "trimmed";
|
||||
for (const entry of this.entries.value) {
|
||||
if (
|
||||
entry.key === this.activeKey.value ||
|
||||
!entry.mounted ||
|
||||
entry.status !== "inactive"
|
||||
)
|
||||
continue;
|
||||
if (!options.includePinned && entry.route.meta.native?.cache === "pin")
|
||||
continue;
|
||||
this.evict(entry, reason);
|
||||
}
|
||||
this.touch();
|
||||
}
|
||||
|
||||
markStatuses(transaction: NativeTransaction | null) {
|
||||
for (const entry of this.entries.value) {
|
||||
if (entry.key === this.activeKey.value) entry.status = "active";
|
||||
else if (transaction?.toKey === entry.key && entry.status === "preview")
|
||||
entry.status = "preview";
|
||||
else if (entry.mounted) entry.status = "inactive";
|
||||
else entry.status = "evicted";
|
||||
}
|
||||
this.touch();
|
||||
}
|
||||
|
||||
removePreview(key: string) {
|
||||
const entry = this.byKey(key);
|
||||
if (!entry || entry.key === this.activeKey.value) return;
|
||||
if (entry.status === "preview" || entry.synthetic) {
|
||||
if (entry.synthetic || !entry.committed) {
|
||||
this.entries.value = this.entries.value.filter(
|
||||
(candidate) => candidate.key !== key,
|
||||
);
|
||||
} else {
|
||||
entry.status = "inactive";
|
||||
this.touch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discardTarget(key: string, reason: NativeEvictionReason) {
|
||||
const entry = this.byKey(key);
|
||||
if (!entry || entry.key === this.activeKey.value) return;
|
||||
if (!entry.committed || entry.synthetic) {
|
||||
this.entries.value = this.entries.value.filter(
|
||||
(candidate) => candidate.key !== key,
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.evict(entry, reason);
|
||||
}
|
||||
|
||||
enforceCache() {
|
||||
const protectedUnderlayKeys = new Set<string>();
|
||||
let presentedEntry = this.active();
|
||||
while (
|
||||
presentedEntry?.presentation === "sheet" &&
|
||||
presentedEntry.underlayKey &&
|
||||
!protectedUnderlayKeys.has(presentedEntry.underlayKey)
|
||||
) {
|
||||
protectedUnderlayKeys.add(presentedEntry.underlayKey);
|
||||
presentedEntry = this.byKey(presentedEntry.underlayKey);
|
||||
}
|
||||
|
||||
const inactive = this.entries.value
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.key !== this.activeKey.value &&
|
||||
entry.mounted &&
|
||||
entry.status === "inactive",
|
||||
)
|
||||
.sort((a, b) => b.lastUsed - a.lastUsed);
|
||||
|
||||
for (const entry of inactive) {
|
||||
if (protectedUnderlayKeys.has(entry.key)) continue;
|
||||
if (!this.shouldRetainInactive(entry)) {
|
||||
const reason: NativeEvictionReason =
|
||||
entry.route.meta.native?.cache === false
|
||||
? "cache-disabled"
|
||||
: "popped";
|
||||
this.evict(entry, reason);
|
||||
}
|
||||
}
|
||||
|
||||
const retained = inactive.filter(
|
||||
(entry) =>
|
||||
entry.mounted &&
|
||||
entry.route.meta.native?.cache !== "pin" &&
|
||||
!protectedUnderlayKeys.has(entry.key),
|
||||
);
|
||||
for (const entry of retained.slice(this.maxInactive))
|
||||
this.evict(entry, "cache-limit");
|
||||
this.touch();
|
||||
}
|
||||
|
||||
touch() {
|
||||
this.entries.value = [...this.entries.value];
|
||||
}
|
||||
|
||||
private shouldRetainInactive(entry: NativeViewEntry) {
|
||||
const policy = entry.route.meta.native?.cache;
|
||||
if (policy === false) return false;
|
||||
if (policy === "pin") return true;
|
||||
if (this.history.contains(entry.key)) return true;
|
||||
return entry.route.meta.native?.siblingHistory === "replace";
|
||||
}
|
||||
|
||||
private evict(entry: NativeViewEntry, reason: NativeEvictionReason) {
|
||||
if (!entry.mounted || entry.key === this.activeKey.value) return;
|
||||
entry.mounted = false;
|
||||
entry.status = "evicted";
|
||||
entry.evictionReason = reason;
|
||||
this.totalEvictions += 1;
|
||||
this.lastEviction = { key: entry.key, route: entry.route.fullPath, reason };
|
||||
this.diagnostics.emit("view-evicted", {
|
||||
route: diagnosticRoute(entry.route),
|
||||
details: { reason },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user