1 | <script lang="ts"> |
2 | import PageContainer from "$lib/components/PageContainer.svelte"; |
3 | import { onMount } from "svelte"; |
4 | import { LineNumbers, HighlightSvelte } from "svelte-highlight"; |
5 | import 'svelte-highlight/styles/github-dark.css'; |
6 |
|
7 | export let data; |
8 |
|
9 | const MAX_LINES = 20; |
10 | const INITIAL_TIMEOUT = 500; |
11 |
|
12 | function generateAndInjectCSSOpacity(n: number) { |
13 | let css = ""; |
14 | for (let i = 1; i <= n; i++) { |
15 | let opacity = Math.max(0, 1 - (i - 1) * (1 / (n - 1))); |
16 | css += `#line:nth-child(${i}) {opacity: ${opacity.toFixed(2)};}\n`; |
17 | } |
18 |
|
19 | let style = document.createElement("style"); |
20 | style.textContent = css; |
21 | document.head.appendChild(style); |
22 | } |
23 |
|
24 | function makeDebounce<K, R>(func: (arg1: K) => R, timeout: number) { |
25 | let timeoutId: number | undefined; |
26 | let lastCallTimestamp: number | undefined; |
27 | let rejectLastCall: (reason?: any) => void; |
28 |
|
29 | return { |
30 | adjustTimeout: (value: number) => { |
31 | timeout = value; |
32 | window.clearTimeout(timeoutId); |
33 | lastCallTimestamp = undefined; |
34 | }, |
35 | getLastCallTimestamp: () => lastCallTimestamp, |
36 | fn: (input: K) => { |
37 | if (timeoutId) { |
38 | window.clearTimeout(timeoutId); |
39 | rejectLastCall(`💀 Cleared timeout: ${timeoutId}`); |
40 | } |
41 | return new Promise<R>((res, rej) => { |
42 | rejectLastCall = rej; |
43 | lastCallTimestamp = performance.now(); |
44 | timeoutId = window.setTimeout(() => { |
45 | lastCallTimestamp = undefined; |
46 | res(func(input)); |
47 | }, timeout); |
48 | }); |
49 | }, |
50 | }; |
51 | } |
52 |
|
53 | function makeAbortableFetch() { |
54 | let controller: AbortController | undefined; |
55 | let id = 0; |
56 | return async (input: RequestInfo | URL, init?: RequestInit) => { |
57 | if (controller) { |
58 | controller.abort(); |
59 | } |
60 | const currentId = id++; |
61 | controller = new AbortController(); |
62 | appendOutput(`🚀 Fired API ${currentId}`); |
63 | return fetch(input, { |
64 | ...(init || {}), |
65 | signal: controller.signal, |
66 | }).catch((err) => { |
67 | if (err instanceof DOMException) { |
68 | if (err.name === "AbortError") { |
69 | appendOutput( |
70 | `💀 The API call ${currentId} was aborted`, |
71 | ); |
72 | } |
73 | } |
74 | return err; |
75 | }); |
76 | }; |
77 | } |
78 |
|
79 | function appendOutput(str: string) { |
80 | const output = document.getElementById("output"); |
81 | const child = document.createElement("div"); |
82 | child.setAttribute("id", "line"); |
83 | child.innerHTML = str; |
84 | output?.insertBefore(child, output.firstChild); |
85 | while (output && output.children.length >= MAX_LINES) { |
86 | output.removeChild(output.lastElementChild as Node); |
87 | } |
88 | } |
89 |
|
90 | onMount(() => { |
91 | let timeout = INITIAL_TIMEOUT; |
92 | const abortableFetch = makeAbortableFetch(); |
93 | let { |
94 | fn: abortableFetchDebounced, |
95 | adjustTimeout, |
96 | getLastCallTimestamp, |
97 | } = makeDebounce( |
98 | (data: string) => () => |
99 | abortableFetch(`https://echo.free.beeceptor.com?query=${data}`), |
100 | timeout, |
101 | ); |
102 |
|
103 | const timer = document.getElementById("timer"); |
104 |
|
105 | setInterval(() => { |
106 | const val = getLastCallTimestamp(); |
107 | if (val) { |
108 | const elapsedMs = performance.now() - val; |
109 | timer?.style.setProperty( |
110 | "width", |
111 | `${100 * (elapsedMs / timeout)}%`, |
112 | ); |
113 | timer?.style.setProperty("background-color", "gray"); |
114 | } else { |
115 | timer?.style.setProperty("width", "100%"); |
116 | timer?.style.setProperty("background-color", "green"); |
117 | } |
118 | }, 1000 / 120); |
119 |
|
120 | (document.getElementById("timeout") as HTMLInputElement).value = |
121 | String(timeout); |
122 | document.getElementById("timeout")?.addEventListener("input", (e) => { |
123 | const target = e.target as HTMLInputElement; |
124 | timeout = parseInt(target.value, 10); |
125 | adjustTimeout(timeout); |
126 | }); |
127 |
|
128 | document.getElementById("query")?.addEventListener("input", (e) => { |
129 | const target = e.target as HTMLInputElement; |
130 | abortableFetchDebounced(target.value) |
131 | .then((dispatchedFetch) => { |
132 | dispatchedFetch().then(async (res) => { |
133 | const data = await res.json(); |
134 | appendOutput( |
135 | `✅ ${JSON.stringify(data.parsedQueryParams)}`, |
136 | ); |
137 | }); |
138 | }) |
139 | .catch((err) => appendOutput(err)); |
140 | }); |
141 |
|
142 | generateAndInjectCSSOpacity(MAX_LINES); |
143 | }); |
144 | </script> |
145 |
|
146 | <svelte:head> |
147 | <style> |
148 | #form { |
149 | display: flex; |
150 | flex-direction: column; |
151 | padding: 20px; |
152 | gap: 5px; |
153 | background-color: black; |
154 | } |
155 |
|
156 | #timer { |
157 | min-height: 20px; |
158 | border-radius: 5px; |
159 | width: 0%; |
160 | } |
161 |
|
162 | #output { |
163 | background-color: #2d3235; |
164 | padding: 10px; |
165 | display: flex; |
166 | flex-direction: column; |
167 | gap: 5px; |
168 | width: 100%; |
169 | } |
170 | </style> |
171 | </svelte:head> |
172 |
|
173 | <PageContainer> |
174 | <div> |
175 | <div class="text-2xl font-bold text-secondary"> |
176 | {data.title} |
177 | </div> |
178 | <div class="text-sm font-bold text-primary"> |
179 | Written: {data.written} |
180 | </div> |
181 |
|
182 | <p class="first-letter:text-2xl"> |
183 | Javascript closures are simply stateful functions. Here's an example |
184 | chaining a debounce closure with a abortable fetch closure. |
185 | </p> |
186 | <p> |
187 | Note that even though this website is written in sveltekit, the example javascript uses straigh DOM manipulation |
188 | to demonstrate the details. |
189 | </p> |
190 |
|
191 | <div id="form"> |
192 | <h2>1. Adjust API Timeout (ms)</h2> |
193 | <input class="input input-primary" id="timeout" type="text" /> |
194 | <h2>2. Write something</h2> |
195 | <input class="input input-primary" id="query" type="text" /> |
196 | <div id="timer"></div> |
197 | <h2>Output:</h2> |
198 | <div id="output"></div> |
199 | </div> |
200 |
|
201 | <div> |
202 | <h2 class="text-2xl text-secondary">Svelte Source (This page)</h2> |
203 | <HighlightSvelte |
204 | code={data["sveltePageSource"]} |
205 | let:highlighted |
206 | > |
207 | <LineNumbers {highlighted} /> |
208 | </HighlightSvelte> |
209 | <pre><code class="hljs language-html"></code></pre> |
210 | </div> |
211 | </div> |
212 | </PageContainer> |
213 |
|