manages browsing session history jsr.io/@mary/history
typescript jsr

feat: history stack

mary.my.id 4d217d5d eb512612

verified
Changed files
+119 -1
lib
+4 -1
deno.json
··· 2 2 "name": "@mary/history", 3 3 "version": "0.1.1", 4 4 "license": "0BSD", 5 - "exports": "./lib/mod.ts", 5 + "exports": { 6 + ".": "./lib/mod.ts", 7 + "./stack": "./lib/history-stack.ts" 8 + }, 6 9 "fmt": { 7 10 "useTabs": true, 8 11 "indentWidth": 2,
+115
lib/history-stack.ts
··· 1 + /** 2 + * @module 3 + * provides an implementation that maintains a best-effort snapshot of the 4 + * browser's history stack. 5 + */ 6 + import type { BrowserHistory, Location } from './mod.ts'; 7 + 8 + /** 9 + * represents a read-only view of a location stack synchronized with browser history. 10 + */ 11 + export interface HistoryStack { 12 + /** 13 + * current location entry. 14 + */ 15 + readonly current: Location; 16 + /** 17 + * index of the active entry. 18 + */ 19 + readonly active: number; 20 + /** 21 + * list of entries; null denotes a gap in entries. 22 + */ 23 + readonly entries: (Location | null)[]; 24 + /** 25 + * whether a backward traversal is possible. 26 + */ 27 + readonly canGoBack: boolean; 28 + /** 29 + * whether a forward traversal is possible. 30 + */ 31 + readonly canGoForward: boolean; 32 + } 33 + 34 + /** 35 + * constructs a derived stack view from a browser history instance. 36 + * @param history source history to observe 37 + * @param keepForwardEntries when true, preserves forward entries during backward traversal; when false, forward entries may be dropped 38 + * @returns a live history stack view 39 + */ 40 + export const createHistoryStack = (history: BrowserHistory, keepForwardEntries = true): HistoryStack => { 41 + const loc = history.location; 42 + 43 + let active = loc.index; 44 + let entries = arr(active + 1, (i) => (i === active ? loc : null)); 45 + 46 + history.listen(({ action, location }) => { 47 + const index = location.index; 48 + 49 + if (action === 'push') { 50 + // new page pushed 51 + 52 + entries = entries.toSpliced(active + 1, entries.length, location); 53 + } else if (action === 'replace' || action === 'update') { 54 + // current page replaced, or updated with new state 55 + 56 + entries = entries.with(active, location); 57 + } else if (action === 'traverse') { 58 + // traversal happened 59 + 60 + if (keepForwardEntries) { 61 + if (index >= entries.length) { 62 + const length = entries.length; 63 + const delta = index - length; 64 + 65 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 66 + 67 + entries = entries.concat(extras); 68 + } else if (entries[index] === null) { 69 + entries = entries.with(index, location); 70 + } 71 + } else { 72 + if (index < active) { 73 + if (entries[index] !== null) { 74 + entries = entries.slice(0, index + 1); 75 + } else { 76 + entries = entries.toSpliced(index, entries.length, location); 77 + } 78 + } else if (index >= entries.length) { 79 + const length = entries.length; 80 + const delta = index - length; 81 + 82 + const extras = arr(delta + 1, (i) => (i === delta ? location : null)); 83 + 84 + entries = entries.concat(extras); 85 + } 86 + } 87 + } 88 + 89 + active = index; 90 + }); 91 + 92 + return { 93 + get current() { 94 + // current entry is guaranteed to exist 95 + return entries[active]!; 96 + }, 97 + get active() { 98 + return active; 99 + }, 100 + get entries() { 101 + return entries; 102 + }, 103 + 104 + get canGoBack() { 105 + return active !== 0; 106 + }, 107 + get canGoForward() { 108 + return active !== entries.length - 1; 109 + }, 110 + }; 111 + }; 112 + 113 + const arr = <T>(length: number, map: (index: number) => T): T[] => { 114 + return Array.from({ length }, (_, idx) => map(idx)); 115 + };