handy online tools for AT Protocol
boat.kelinci.net
atproto
bluesky
atcute
typescript
solidjs
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};