-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathshort-history.mjs
More file actions
115 lines (101 loc) · 3.53 KB
/
Copy pathshort-history.mjs
File metadata and controls
115 lines (101 loc) · 3.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
export function buildShortPlaybackQueue({
videos = [],
requestedVideo = null,
isHidden = () => false,
allowHiddenRequested = false,
} = {}) {
const playable = Array.isArray(videos) ? videos.filter((video) => video?.id) : [];
const requestedId = requestedVideo?.id;
const requestedIndex = playable.findIndex((video) => video.id === requestedId);
const selectedVideo = requestedIndex >= 0 ? playable[requestedIndex] : null;
const afterRequested = requestedIndex >= 0 ? playable.slice(requestedIndex + 1) : playable;
const firstVisible = afterRequested.find((video) => !isHidden(video.id))
|| playable.find((video) => !isHidden(video.id))
|| null;
const startVideo = selectedVideo && (!isHidden(selectedVideo.id) || allowHiddenRequested)
? selectedVideo
: firstVisible;
if (!startVideo) return [];
return [
startVideo,
...playable.filter((video) => video.id !== startVideo.id && !isHidden(video.id)),
];
}
export function createTransientDirectionalHistory({
ttlMs = 10_000,
maxEntries = 8,
now = () => Date.now(),
} = {}) {
let backStack = [];
let forwardStack = [];
const playableByDefault = () => true;
function isValid(entry, currentId, isPlayable, at) {
if (!entry?.id || entry.id === currentId) return false;
if (!Number.isFinite(entry.stamp) || at - entry.stamp > ttlMs) return false;
return isPlayable(entry.id);
}
function prune(stack, currentId, isPlayable = playableByDefault, at = now()) {
return stack.filter((entry) => isValid(entry, currentId, isPlayable, at));
}
function push(stack, id, at = now()) {
if (!id) return stack;
const next = stack.filter((entry) => entry.id !== id);
next.push({ id, stamp: at });
return next.slice(-maxEntries);
}
function reset() {
backStack = [];
forwardStack = [];
}
function pushForNext(currentId) {
const at = now();
backStack = prune(backStack, currentId, playableByDefault, at);
backStack = push(backStack, currentId, at);
forwardStack = [];
}
function peekBack(currentId, isPlayable = playableByDefault) {
backStack = prune(backStack, currentId, isPlayable);
return backStack.length ? { ...backStack[backStack.length - 1] } : null;
}
function peekForward(currentId, isPlayable = playableByDefault) {
forwardStack = prune(forwardStack, currentId, isPlayable);
return forwardStack.length ? { ...forwardStack[forwardStack.length - 1] } : null;
}
function back(currentId, isPlayable = playableByDefault) {
const target = peekBack(currentId, isPlayable);
if (!target) return null;
backStack.pop();
forwardStack = push(forwardStack, currentId);
return target;
}
function forward(currentId, isPlayable = playableByDefault) {
const target = peekForward(currentId, isPlayable);
if (!target) return null;
forwardStack.pop();
backStack = push(backStack, currentId);
return target;
}
function nextExpiryAt(currentId, isPlayable = playableByDefault) {
const at = now();
backStack = prune(backStack, currentId, isPlayable, at);
forwardStack = prune(forwardStack, currentId, isPlayable, at);
const expiries = [...backStack, ...forwardStack].map((entry) => entry.stamp + ttlMs);
return expiries.length ? Math.min(...expiries) : null;
}
function snapshot() {
return {
back: backStack.map((entry) => ({ ...entry })),
forward: forwardStack.map((entry) => ({ ...entry })),
};
}
return {
reset,
pushForNext,
peekBack,
peekForward,
back,
forward,
nextExpiryAt,
snapshot,
};
}