handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs
at trunk 161 lines 3.7 kB view raw
1import { For } from 'solid-js'; 2import { createMutable } from 'solid-js/store'; 3import { assert } from '~/lib/utils/invariant'; 4 5interface LogEntry { 6 typ: 'log' | 'info' | 'warn' | 'error'; 7 at: number; 8 msg: string; 9} 10 11interface PendingLogEntry { 12 msg: string; 13} 14 15export const createLogger = () => { 16 const pending = createMutable<PendingLogEntry[]>([]); 17 18 let backlog: LogEntry[] | undefined = []; 19 let push = (entry: LogEntry) => { 20 backlog!.push(entry); 21 }; 22 23 return { 24 internal: { 25 get pending() { 26 return pending; 27 }, 28 attach(fn: (entry: LogEntry) => void) { 29 if (backlog !== undefined) { 30 for (let idx = 0, len = backlog.length; idx < len; idx++) { 31 fn(backlog[idx]); 32 } 33 34 backlog = undefined; 35 } 36 37 push = fn; 38 }, 39 }, 40 log(msg: string) { 41 push({ typ: 'log', at: Date.now(), msg }); 42 }, 43 info(msg: string) { 44 push({ typ: 'info', at: Date.now(), msg }); 45 }, 46 warn(msg: string) { 47 push({ typ: 'warn', at: Date.now(), msg }); 48 }, 49 error(msg: string) { 50 push({ typ: 'error', at: Date.now(), msg }); 51 }, 52 progress(initialMsg: string, throttleMs = 500) { 53 pending.unshift({ msg: initialMsg }); 54 55 let entry: PendingLogEntry | undefined = pending[0]; 56 57 return { 58 update: throttle((msg: string) => { 59 if (entry !== undefined) { 60 entry.msg = msg; 61 } 62 }, throttleMs), 63 destroy() { 64 if (entry !== undefined) { 65 const index = pending.indexOf(entry); 66 67 pending.splice(index, 1); 68 entry = undefined; 69 } 70 }, 71 [Symbol.dispose]() { 72 this.destroy(); 73 }, 74 }; 75 }, 76 }; 77}; 78 79export interface LoggerProps { 80 logger: ReturnType<typeof createLogger>; 81} 82 83const Logger = ({ logger }: LoggerProps) => { 84 const formatter = new Intl.DateTimeFormat('en-US', { timeStyle: 'short', hour12: false }); 85 86 return ( 87 <ul class="flex flex-col py-3 font-mono text-xs empty:hidden"> 88 <For each={logger.internal.pending}> 89 {(entry) => ( 90 <li class="flex gap-2 whitespace-pre-wrap px-4 py-1"> 91 <span class="shrink-0 whitespace-pre-wrap font-medium text-gray-400">-----</span> 92 <span class="break-words">{entry.msg}</span> 93 </li> 94 )} 95 </For> 96 97 <div 98 ref={(node) => { 99 logger.internal.attach(({ typ, at, msg }) => { 100 let ecn = `flex gap-2 whitespace-pre-wrap px-4 py-1`; 101 let tcn = `shrink-0 whitespace-pre-wrap font-medium`; 102 if (typ === 'log') { 103 tcn += ` text-gray-500`; 104 } else if (typ === 'info') { 105 ecn += ` bg-blue-200 text-blue-800`; 106 tcn += ` text-blue-500`; 107 } else if (typ === 'warn') { 108 ecn += ` bg-amber-200 text-amber-800`; 109 tcn += ` text-amber-500`; 110 } else if (typ === 'error') { 111 ecn += ` bg-red-200 text-red-800`; 112 tcn += ` text-red-500`; 113 } 114 115 const item = ( 116 <li class={ecn}> 117 <span class={tcn}>{/* @once */ formatter.format(at)}</span> 118 <span class="break-words">{msg}</span> 119 </li> 120 ); 121 122 assert(item instanceof Node); 123 node.after(item); 124 }); 125 }} 126 ></div> 127 </ul> 128 ); 129}; 130 131export default Logger; 132 133const throttle = <T extends (...args: any[]) => void>(func: T, wait: number) => { 134 let timeout: ReturnType<typeof setTimeout> | null = null; 135 136 let lastArgs: Parameters<T> | null = null; 137 let lastCallTime = 0; 138 139 const invoke = () => { 140 func(...lastArgs!); 141 lastCallTime = Date.now(); 142 timeout = null; 143 }; 144 145 return (...args: Parameters<T>) => { 146 const now = Date.now(); 147 const timeSinceLastCall = now - lastCallTime; 148 149 lastArgs = args; 150 151 if (timeSinceLastCall >= wait) { 152 if (timeout !== null) { 153 clearTimeout(timeout); 154 } 155 156 invoke(); 157 } else if (timeout === null) { 158 timeout = setTimeout(invoke, wait - timeSinceLastCall); 159 } 160 }; 161};