position user search bar results

This commit is contained in:
Dylan Knutson
2025-07-23 17:59:10 +00:00
parent 8333a1bb3f
commit ad229fbd4e
10 changed files with 131 additions and 478 deletions

View File

@@ -4,6 +4,10 @@
"trailingComma": "all", "trailingComma": "all",
"arrowParens": "always", "arrowParens": "always",
"singleQuote": true, "singleQuote": true,
"semi": true,
"bracketSpacing": true,
"bracketSameLine": false,
"printWidth": 80,
"plugins": [ "plugins": [
"prettier-plugin-tailwindcss", "prettier-plugin-tailwindcss",
"@prettier/plugin-ruby", "@prettier/plugin-ruby",
@@ -11,5 +15,13 @@
"@4az/prettier-plugin-html-erb" "@4az/prettier-plugin-html-erb"
], ],
"xmlQuoteAttributes": "double", "xmlQuoteAttributes": "double",
"xmlWhitespaceSensitivity": "ignore" "xmlWhitespaceSensitivity": "ignore",
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"options": {
"parser": "typescript"
}
}
]
} }

View File

@@ -29,11 +29,11 @@ export default function ListItem({
subtext, subtext,
domainIcon, domainIcon,
}: PropTypes) { }: PropTypes) {
const groupHoverClassName = 'group-hover:text-slate-200';
const iconClassName = ['ml-2']; const iconClassName = ['ml-2'];
const textClassName = [ const textClassName = [
COMMON_LIST_ELEM_CLASSES, COMMON_LIST_ELEM_CLASSES,
'relative flex items-center justify-between', 'group flex items-center justify-between',
'border-t-0',
isLast && 'rounded-b-lg', isLast && 'rounded-b-lg',
style === 'item' && selected && 'bg-slate-700 text-slate-100', style === 'item' && selected && 'bg-slate-700 text-slate-100',
style === 'info' && 'text-slate-500 italic', style === 'info' && 'text-slate-500 italic',
@@ -66,12 +66,21 @@ export default function ListItem({
</div> </div>
<div className="inline-block flex-grow pl-1">{value}</div> <div className="inline-block flex-grow pl-1">{value}</div>
{subtext && ( {subtext && (
<div className="vertical-align-middle float-right inline-block pl-1 text-sm italic text-slate-500"> <div
className={[
'vertical-align-middle float-right inline-block pl-1 text-sm italic',
!selected && 'text-slate-500',
selected && 'text-slate-300',
groupHoverClassName,
]
.filter(Boolean)
.join(' ')}
>
{subtext} {subtext}
</div> </div>
)} )}
{domainIcon && ( {domainIcon && (
<img src={domainIcon} alt="domain icon" className="inline w-6 pl-1" /> <img src={domainIcon} alt="domain icon" className="inline w-6" />
)} )}
</div> </div>
</a> </a>

View File

@@ -44,9 +44,9 @@ export const PostHoverPreviewWrapper: React.FC<
className={anchorClassNamesForVisualStyle(visualStyle, true)} className={anchorClassNamesForVisualStyle(visualStyle, true)}
> >
{postDomainIcon && ( {postDomainIcon && (
<img <img
src={postDomainIcon} src={postDomainIcon}
alt={postThumbnailAlt} alt={postThumbnailAlt}
className={iconClassNamesForSize('small')} className={iconClassNamesForSize('small')}
/> />
)} )}

View File

@@ -44,7 +44,7 @@ export const SortableTable: React.FC<SortableTableProps> = ({
}; };
const sortedData = React.useMemo(() => { const sortedData = React.useMemo(() => {
const headerIndex = headers.findIndex(h => h.key === sortKey); const headerIndex = headers.findIndex((h) => h.key === sortKey);
if (headerIndex === -1) return data; if (headerIndex === -1) return data;
return [...data].sort((a, b) => { return [...data].sort((a, b) => {
@@ -92,7 +92,13 @@ export const SortableTable: React.FC<SortableTableProps> = ({
const getSortIndicator = (headerKey: string) => { const getSortIndicator = (headerKey: string) => {
if (sortKey !== headerKey) { if (sortKey !== headerKey) {
return <span style={{ opacity: 0.5, marginLeft: '0.25rem', fontSize: '0.75rem' }}></span>; return (
<span
style={{ opacity: 0.5, marginLeft: '0.25rem', fontSize: '0.75rem' }}
>
</span>
);
} }
return ( return (
<span style={{ marginLeft: '0.25rem', fontSize: '0.75rem' }}> <span style={{ marginLeft: '0.25rem', fontSize: '0.75rem' }}>
@@ -112,7 +118,9 @@ export const SortableTable: React.FC<SortableTableProps> = ({
style={{ style={{
...headerStyle, ...headerStyle,
textAlign: header.align, textAlign: header.align,
...(index === headers.length - 1 ? { borderRight: 'none' } : {}), ...(index === headers.length - 1
? { borderRight: 'none' }
: {}),
}} }}
onClick={() => handleSort(header.key)} onClick={() => handleSort(header.key)}
onMouseEnter={(e) => { onMouseEnter={(e) => {
@@ -130,7 +138,7 @@ export const SortableTable: React.FC<SortableTableProps> = ({
{/* Data Rows */} {/* Data Rows */}
{sortedData.map((row) => ( {sortedData.map((row) => (
<div key={row.id} className="contents group"> <div key={row.id} className="group contents">
{row.cells.map((cell, cellIndex) => ( {row.cells.map((cell, cellIndex) => (
<div <div
key={cellIndex} key={cellIndex}
@@ -140,9 +148,11 @@ export const SortableTable: React.FC<SortableTableProps> = ({
fontSize: '0.875rem', fontSize: '0.875rem',
fontWeight: cellIndex === 0 ? 500 : 400, fontWeight: cellIndex === 0 ? 500 : 400,
color: cellIndex === 0 ? '#0f172a' : '#64748b', color: cellIndex === 0 ? '#0f172a' : '#64748b',
...(cellIndex === row.cells.length - 1 ? { borderRight: 'none' } : {}), ...(cellIndex === row.cells.length - 1
? { borderRight: 'none' }
: {}),
}} }}
className="group-hover:bg-slate-50 transition-colors duration-150" className="transition-colors duration-150 group-hover:bg-slate-50"
> >
{cell.value} {cell.value}
</div> </div>

View File

@@ -28,7 +28,8 @@ export const StatsCard: React.FC<StatsCardProps> = ({
<div className="text-xl font-bold text-slate-900"> <div className="text-xl font-bold text-slate-900">
{requestCount} requests {requestCount} requests
<span className="text-base font-normal text-slate-600"> <span className="text-base font-normal text-slate-600">
{' '}in last {timeWindow} {' '}
in last {timeWindow}
</span> </span>
</div> </div>
<div className="mt-1 text-slate-600"> <div className="mt-1 text-slate-600">

View File

@@ -42,117 +42,137 @@ export const StatsPage: React.FC<StatsPageProps> = ({
byDomainCounts, byDomainCounts,
availableTimeWindows, availableTimeWindows,
}) => { }) => {
const contentTypeData: TableData[] = contentTypeCounts.map(item => ({ const contentTypeData: TableData[] = contentTypeCounts.map((item) => ({
id: item.content_type, id: item.content_type,
cells: [ cells: [
{ value: item.content_type, sortKey: item.content_type }, { value: item.content_type, sortKey: item.content_type },
{ value: item.countFormatted, sortKey: item.count }, { value: item.countFormatted, sortKey: item.count },
{ value: item.bytesFormatted, sortKey: item.bytes } { value: item.bytesFormatted, sortKey: item.bytes },
] ],
})); }));
const domainData: TableData[] = byDomainCounts.map(item => ({ const domainData: TableData[] = byDomainCounts.map((item) => ({
id: item.domain, id: item.domain,
cells: [ cells: [
{ value: item.domain, sortKey: item.domain }, { value: item.domain, sortKey: item.domain },
{ value: item.countFormatted, sortKey: item.count }, { value: item.countFormatted, sortKey: item.count },
{ value: item.bytesFormatted, sortKey: item.bytes } { value: item.bytesFormatted, sortKey: item.bytes },
] ],
})); }));
return ( return (
<div className="mx-auto max-w-7xl px-4 mt-8"> <div className="mx-auto mt-8 max-w-7xl px-4">
{/* Header Section */} {/* Header Section */}
<div className="bg-white border border-slate-200 rounded-xl shadow-sm overflow-hidden"> <div className="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
{/* Top Bar */} {/* Top Bar */}
<div className="bg-slate-50 px-6 py-4 border-b border-slate-200"> <div className="border-b border-slate-200 bg-slate-50 px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-slate-900">HTTP Request Analytics</h1> <h1 className="text-2xl font-bold text-slate-900">
<a HTTP Request Analytics
href="/log_entries" </h1>
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-white border border-slate-300 rounded-lg transition-colors hover:shadow-sm" <a
> href="/log_entries"
<i className="fas fa-arrow-left" /> className="inline-flex items-center gap-2 rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-600 transition-colors hover:bg-white hover:text-slate-900 hover:shadow-sm"
Back to Log Entries >
</a> <i className="fas fa-arrow-left" />
Back to Log Entries
</a>
</div> </div>
</div> </div>
{/* Stats Summary */} {/* Stats Summary */}
<div className="px-6 py-6 bg-gradient-to-br from-blue-50 to-indigo-50 border-t border-slate-200"> <div className="border-t border-slate-200 bg-gradient-to-br from-blue-50 to-indigo-50 px-6 py-6">
<div className="text-center mb-4"> <div className="mb-4 text-center">
<h3 className="text-lg font-semibold text-slate-900 mb-1">Summary for {timeWindowFormatted}</h3> <h3 className="mb-1 text-lg font-semibold text-slate-900">
Summary for {timeWindowFormatted}
</h3>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4"> <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{/* Total Requests */} {/* Total Requests */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-slate-200"> <div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Total Requests</p> <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
<p className="text-2xl font-bold text-slate-900 mt-1">{lastWindowCount.toLocaleString()}</p> Total Requests
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">
{lastWindowCount.toLocaleString()}
</p>
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
<i className="fas fa-chart-bar text-blue-600" />
</div> </div>
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<i className="fas fa-chart-bar text-blue-600" />
</div>
</div> </div>
</div> </div>
{/* Requests per Second */} {/* Requests per Second */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-slate-200"> <div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Requests/sec</p> <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
<p className="text-2xl font-bold text-slate-900 mt-1">{requestsPerSecond}</p> Requests/sec
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">
{requestsPerSecond}
</p>
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
<i className="fas fa-bolt text-green-600" />
</div> </div>
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<i className="fas fa-bolt text-green-600" />
</div>
</div> </div>
</div> </div>
{/* Total Data */} {/* Total Data */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-slate-200"> <div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Total Data</p> <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
<p className="text-2xl font-bold text-slate-900 mt-1">{totalBytesFormatted}</p> Total Data
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">
{totalBytesFormatted}
</p>
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100">
<i className="fas fa-database text-purple-600" />
</div> </div>
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<i className="fas fa-database text-purple-600" />
</div>
</div> </div>
</div> </div>
{/* Data per Second */} {/* Data per Second */}
<div className="bg-white rounded-lg p-4 shadow-sm border border-slate-200"> <div className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-xs font-medium text-slate-500 uppercase tracking-wide">Data/sec</p> <p className="text-xs font-medium uppercase tracking-wide text-slate-500">
<p className="text-2xl font-bold text-slate-900 mt-1">{bytesPerSecondFormatted}</p> Data/sec
</p>
<p className="mt-1 text-2xl font-bold text-slate-900">
{bytesPerSecondFormatted}
</p>
</div>
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100">
<i className="fas fa-download text-orange-600" />
</div> </div>
<div className="w-10 h-10 bg-orange-100 rounded-lg flex items-center justify-center">
<i className="fas fa-download text-orange-600" />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Time Window Selector */} {/* Time Window Selector */}
<div className="px-4 py-4 bg-gradient-to-br from-blue-50 to-indigo-50"> <div className="bg-gradient-to-br from-blue-50 to-indigo-50 px-4 py-4">
<div className="text-center"> <div className="text-center">
<div className="inline-flex flex-wrap justify-center bg-white/70 backdrop-blur-sm rounded-lg p-1 shadow-md border border-white/50 gap-1"> <div className="inline-flex flex-wrap justify-center gap-1 rounded-lg border border-white/50 bg-white/70 p-1 shadow-md backdrop-blur-sm">
{availableTimeWindows.map((timeWindowOption, index) => ( {availableTimeWindows.map((timeWindowOption, index) => (
<React.Fragment key={timeWindowOption.seconds}> <React.Fragment key={timeWindowOption.seconds}>
{timeWindowOption.active ? ( {timeWindowOption.active ? (
<span className="px-4 py-2 text-sm font-semibold text-white bg-blue-600 rounded-md shadow-sm"> <span className="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm">
{timeWindowOption.label} {timeWindowOption.label}
</span> </span>
) : ( ) : (
<a <a
href={timeWindowOption.path} href={timeWindowOption.path}
className="px-4 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-white hover:shadow-sm rounded-md transition-colors border border-transparent hover:border-slate-200" className="rounded-md border border-transparent px-4 py-2 text-sm font-medium text-slate-600 transition-colors hover:border-slate-200 hover:bg-white hover:text-slate-900 hover:shadow-sm"
> >
{timeWindowOption.label} {timeWindowOption.label}
</a> </a>
@@ -165,14 +185,16 @@ export const StatsPage: React.FC<StatsPageProps> = ({
</div> </div>
{/* Tables Grid - 2 columns */} {/* Tables Grid - 2 columns */}
<div className="my-8 grid grid-cols-1 lg:grid-cols-2 gap-8"> <div className="my-8 grid grid-cols-1 gap-8 lg:grid-cols-2">
<div> <div>
<h2 className="text-xl font-bold text-slate-900 mb-3">By Content Type</h2> <h2 className="mb-3 text-xl font-bold text-slate-900">
By Content Type
</h2>
<SortableTable <SortableTable
headers={[ headers={[
{ label: 'Content Type', key: 'content_type', align: 'left' }, { label: 'Content Type', key: 'content_type', align: 'left' },
{ label: 'Requests', key: 'count', align: 'right' }, { label: 'Requests', key: 'count', align: 'right' },
{ label: 'Transferred', key: 'bytes', align: 'right' } { label: 'Transferred', key: 'bytes', align: 'right' },
]} ]}
data={contentTypeData} data={contentTypeData}
defaultSortKey="count" defaultSortKey="count"
@@ -181,12 +203,12 @@ export const StatsPage: React.FC<StatsPageProps> = ({
</div> </div>
<div> <div>
<h2 className="text-xl font-bold text-slate-900 mb-3">By Domain</h2> <h2 className="mb-3 text-xl font-bold text-slate-900">By Domain</h2>
<SortableTable <SortableTable
headers={[ headers={[
{ label: 'Domain', key: 'domain', align: 'left' }, { label: 'Domain', key: 'domain', align: 'left' },
{ label: 'Requests', key: 'count', align: 'right' }, { label: 'Requests', key: 'count', align: 'right' },
{ label: 'Transferred', key: 'bytes', align: 'right' } { label: 'Transferred', key: 'bytes', align: 'right' },
]} ]}
data={domainData} data={domainData}
defaultSortKey="bytes" defaultSortKey="bytes"

View File

@@ -3,7 +3,6 @@ import * as React from 'react';
import { useCallback, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import Icon from './Icon'; import Icon from './Icon';
import ListItem from './ListItem'; import ListItem from './ListItem';
import Trie, { TrieNode } from '../lib/Trie';
// 1. Group related constants // 1. Group related constants
const CONFIG = { const CONFIG = {
@@ -18,7 +17,7 @@ const STYLES = {
], ],
SVG_BASE_CLASSNAME: `stroke-slate-500 fill-slate-500`, SVG_BASE_CLASSNAME: `stroke-slate-500 fill-slate-500`,
SVG_FOCUSABLE_CLASSNAME: `stroke-slate-500 fill-slate-500 group-focus-within:stroke-slate-800 group-focus-within:fill-slate-800`, SVG_FOCUSABLE_CLASSNAME: `stroke-slate-500 fill-slate-500 group-focus-within:stroke-slate-800 group-focus-within:fill-slate-800`,
INPUT_CLASSNAME: `text-slate-500 group-focus-within:text-slate-800 placeholder-slate-500 group-focus-within:placeholder-slate-800 placeholder:font-extralight`, INPUT_CLASSNAME: `text-slate-500 focus:text-slate-800 placeholder-slate-500 group-focus-within:placeholder-slate-800 placeholder:font-extralight`,
} as const; } as const;
// 2. Simplify logging // 2. Simplify logging
@@ -46,10 +45,6 @@ interface ServerResponse {
users: User[]; users: User[];
} }
type TrieValue = [number, string];
type TrieType = Trie<TrieValue>;
type TrieNodeType = TrieNode<TrieValue>;
export default function UserSearchBar({ isServerRendered }: PropTypes) { export default function UserSearchBar({ isServerRendered }: PropTypes) {
isServerRendered = !!isServerRendered; isServerRendered = !!isServerRendered;
const [pendingRequest, setPendingRequest] = useState<AbortController | null>( const [pendingRequest, setPendingRequest] = useState<AbortController | null>(
@@ -119,7 +114,6 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
} }
} catch (err) { } catch (err) {
if (!err.message.includes('aborted')) { if (!err.message.includes('aborted')) {
log.error('error loading user trie: ', err);
setState((s) => ({ setState((s) => ({
...s, ...s,
errorMessage: `error loading users: ` + err.message, errorMessage: `error loading users: ` + err.message,
@@ -148,7 +142,6 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
const searchForUserDebounced = useCallback( const searchForUserDebounced = useCallback(
debounce(async (userName) => { debounce(async (userName) => {
log.info('sending search for ', userName);
setState((s) => ({ ...s, typingSettled: true })); setState((s) => ({ ...s, typingSettled: true }));
searchForUser(userName); searchForUser(userName);
}, 250), }, 250),
@@ -158,7 +151,6 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
function invokeIdx(idx) { function invokeIdx(idx) {
const user = state.userList[idx]; const user = state.userList[idx];
if (user) { if (user) {
log.info('selecting user: ', user);
setState((s) => ({ ...s, userName: user.name })); setState((s) => ({ ...s, userName: user.name }));
inputRef.current.value = user.name; inputRef.current.value = user.name;
window.location.href = user.show_path; window.location.href = user.show_path;
@@ -185,9 +177,7 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
function UserSearchBarItems() { function UserSearchBarItems() {
return ( return (
<div <div className="divide-y divide-slate-300 overflow-hidden border border-slate-300 bg-slate-50 shadow-lg sm:rounded-xl">
className={`${anyShown || 'border-b-0'} divide-y divide-inherit rounded-b-lg border border-t-0 border-inherit`}
>
{visibility.error ? ( {visibility.error ? (
<ListItem <ListItem
key="error" key="error"
@@ -267,7 +257,7 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
return ( return (
<div <div
className={[ className={[
'group mx-auto w-full p-2 transition-colors duration-1000 sm:rounded-xl', 'relative mx-auto w-full p-2 transition-colors duration-1000 sm:rounded-xl',
'focus-within:border-slate-400 sm:max-w-md', 'focus-within:border-slate-400 sm:max-w-md',
'border-slate-300 bg-slate-50 p-2 shadow-lg', 'border-slate-300 bg-slate-50 p-2 shadow-lg',
].join(' ')} ].join(' ')}
@@ -290,7 +280,6 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
STYLES.INPUT_CLASSNAME, STYLES.INPUT_CLASSNAME,
'rounded-lg outline-none', 'rounded-lg outline-none',
'bg-slate-50 placeholder:italic', 'bg-slate-50 placeholder:italic',
anyShown && 'rounded-b-none',
] ]
.filter(Boolean) .filter(Boolean)
.join(' ')} .join(' ')}
@@ -306,7 +295,11 @@ export default function UserSearchBar({ isServerRendered }: PropTypes) {
ref={inputRef} ref={inputRef}
/> />
</label> </label>
<UserSearchBarItems /> {anyShown && (
<div className="absolute left-0 right-0 top-full z-50 mt-1">
<UserSearchBarItems />
</div>
)}
</div> </div>
); );
} }

View File

@@ -1,91 +0,0 @@
interface SerializedTrie<T> {
// terminal node?
t: 1 | 0;
// value of the node
v: T;
// optional children
c?: { [s: string]: SerializedTrie<T> };
}
export class TrieNode<T> {
public terminal: boolean;
public value: T;
public children: Map<string, TrieNode<T>>;
public serialized: SerializedTrie<T>;
constructor(ser: SerializedTrie<T>) {
this.terminal = ser.t == 1;
this.value = ser.v;
this.children = new Map();
this.serialized = ser;
if (ser.c != null) {
for (const [key, value] of Object.entries(ser.c)) {
this.children.set(key, new TrieNode(value));
}
}
}
}
export default class Trie<T> {
public root: TrieNode<T>;
constructor(ser: SerializedTrie<T>) {
this.root = new TrieNode(ser);
}
public nodeForPrefix(key: string): {
chain: string[];
node: TrieNode<T> | null;
} {
let chain = [];
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let exactChild = null;
console.log('remaining: ', remaining);
for (const [childKey, child] of node.children.entries()) {
if (remaining.startsWith(childKey)) {
console.log('exact match for: ', childKey);
exactChild = child;
chain.push(childKey);
remaining = remaining.slice(childKey.length);
break;
}
}
// if an exact match was found, continue iterating
if (exactChild) {
node = exactChild;
continue;
}
console.log('looking for partial match for ', remaining);
for (const [childKey, child] of node.children.entries()) {
const startsWith = childKey.startsWith(remaining);
console.log(
'test ',
childKey,
' against ',
remaining,
': ',
startsWith,
' ',
child.serialized,
);
if (startsWith) {
console.log('partial match for: ', remaining, ': ', child.serialized);
chain.push(childKey);
return { chain, node: child };
}
}
console.log('did not find partial, bailing!');
return { chain, node: null };
}
// // return remaining.length === 0 && node && node.terminal ? node : null;
console.log('returning child ', node, ' for remaining ', remaining);
return { chain, node };
}
}

View File

@@ -1,140 +0,0 @@
function buildUsersTrie(users) {
const rootNode = new trie();
users.forEach(([id, name]) => {
rootNode.insert(name.toLowerCase(), [id, name]);
});
return JSON.stringify(rootNode.serialize());
}
class trie_node {
constructor() {
this.terminal = false;
this.children = new Map();
}
serialize() {
const { terminal, value, children } = this;
let mapped = {};
let numChildren = 0;
Object.keys(Object.fromEntries(children)).forEach((childKey) => {
numChildren += 1;
mapped[childKey] = children.get(childKey).serialize();
});
return {
t: this.terminal ? 1 : 0,
v: value,
c: numChildren > 0 ? mapped : undefined,
};
}
}
class trie {
constructor() {
this.root = new trie_node();
this.elements = 0;
}
serialize() {
return this.root.serialize();
}
get length() {
return this.elements;
}
get(key) {
const node = this.getNode(key);
if (node) {
return node.value;
}
return null;
}
contains(key) {
const node = this.getNode(key);
return !!node;
}
insert(key, value) {
let node = this.root;
let remaining = key;
while (remaining.length > 0) {
let child = null;
for (const childKey of node.children.keys()) {
const prefix = this.commonPrefix(remaining, childKey);
if (!prefix.length) {
continue;
}
if (prefix.length === childKey.length) {
// enter child node
child = node.children.get(childKey);
remaining = remaining.slice(childKey.length);
break;
}
else {
// split the child
child = new trie_node();
child.children.set(childKey.slice(prefix.length), node.children.get(childKey));
node.children.delete(childKey);
node.children.set(prefix, child);
remaining = remaining.slice(prefix.length);
break;
}
}
if (!child && remaining.length) {
child = new trie_node();
node.children.set(remaining, child);
remaining = "";
}
node = child;
}
if (!node.terminal) {
node.terminal = true;
this.elements += 1;
}
node.value = value;
}
remove(key) {
const node = this.getNode(key);
if (node) {
node.terminal = false;
this.elements -= 1;
}
}
map(prefix, func) {
const mapped = [];
const node = this.getNode(prefix);
const stack = [];
if (node) {
stack.push([prefix, node]);
}
while (stack.length) {
const [key, node] = stack.pop();
if (node.terminal) {
mapped.push(func(key, node.value));
}
for (const c of node.children.keys()) {
stack.push([key + c, node.children.get(c)]);
}
}
return mapped;
}
getNode(key) {
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let child = null;
for (let i = 1; i <= remaining.length; i += 1) {
child = node.children.get(remaining.slice(0, i));
if (child) {
remaining = remaining.slice(i);
break;
}
}
node = child;
}
return remaining.length === 0 && node && node.terminal ? node : null;
}
commonPrefix(a, b) {
const shortest = Math.min(a.length, b.length);
let i = 0;
for (; i < shortest; i += 1) {
if (a[i] !== b[i]) {
break;
}
}
return a.slice(0, i);
}
}

View File

@@ -1,163 +0,0 @@
type UserRow = [number, string];
function buildUsersTrie(users: UserRow[]): string {
const rootNode = new trie<[number, string]>();
users.forEach(([id, name]) => {
rootNode.insert(name.toLowerCase(), [id, name]);
});
return JSON.stringify(rootNode.serialize());
}
class trie_node<T> {
public terminal: boolean;
public value: T;
public children: Map<string, trie_node<T>>;
constructor() {
this.terminal = false;
this.children = new Map();
}
public serialize(): Object {
const { terminal, value, children } = this;
let mapped = {};
let numChildren = 0;
Object.keys(Object.fromEntries(children)).forEach((childKey) => {
numChildren += 1;
mapped[childKey] = children.get(childKey).serialize();
});
return {
t: this.terminal ? 1 : 0,
v: value,
c: numChildren > 0 ? mapped : undefined,
};
}
}
class trie<T> {
public root: trie_node<T>;
public elements: number;
constructor() {
this.root = new trie_node<T>();
this.elements = 0;
}
public serialize(): Object {
return this.root.serialize();
}
public get length(): number {
return this.elements;
}
public get(key: string): T | null {
const node = this.getNode(key);
if (node) {
return node.value;
}
return null;
}
public contains(key: string): boolean {
const node = this.getNode(key);
return !!node;
}
public insert(key: string, value: T): void {
let node = this.root;
let remaining = key;
while (remaining.length > 0) {
let child: trie_node<T> = null;
for (const childKey of node.children.keys()) {
const prefix = this.commonPrefix(remaining, childKey);
if (!prefix.length) {
continue;
}
if (prefix.length === childKey.length) {
// enter child node
child = node.children.get(childKey);
remaining = remaining.slice(childKey.length);
break;
} else {
// split the child
child = new trie_node<T>();
child.children.set(
childKey.slice(prefix.length),
node.children.get(childKey)
);
node.children.delete(childKey);
node.children.set(prefix, child);
remaining = remaining.slice(prefix.length);
break;
}
}
if (!child && remaining.length) {
child = new trie_node<T>();
node.children.set(remaining, child);
remaining = "";
}
node = child;
}
if (!node.terminal) {
node.terminal = true;
this.elements += 1;
}
node.value = value;
}
public remove(key: string): void {
const node = this.getNode(key);
if (node) {
node.terminal = false;
this.elements -= 1;
}
}
public map<U>(prefix: string, func: (key: string, value: T) => U): U[] {
const mapped = [];
const node = this.getNode(prefix);
const stack: [string, trie_node<T>][] = [];
if (node) {
stack.push([prefix, node]);
}
while (stack.length) {
const [key, node] = stack.pop();
if (node.terminal) {
mapped.push(func(key, node.value));
}
for (const c of node.children.keys()) {
stack.push([key + c, node.children.get(c)]);
}
}
return mapped;
}
private getNode(key: string): trie_node<T> | null {
let node = this.root;
let remaining = key;
while (node && remaining.length > 0) {
let child = null;
for (let i = 1; i <= remaining.length; i += 1) {
child = node.children.get(remaining.slice(0, i));
if (child) {
remaining = remaining.slice(i);
break;
}
}
node = child;
}
return remaining.length === 0 && node && node.terminal ? node : null;
}
private commonPrefix(a: string, b: string): string {
const shortest = Math.min(a.length, b.length);
let i = 0;
for (; i < shortest; i += 1) {
if (a[i] !== b[i]) {
break;
}
}
return a.slice(0, i);
}
}