Javascript: Closures
Written: 26/7/2024

Javascript closures are simply stateful functions. Here's an example chaining a debounce closure with a abortable fetch closure.

Note that even though this website is written in sveltekit, the example javascript uses straigh DOM manipulation to demonstrate the details.

1. Adjust API Timeout (ms)

2. Write something

Output:

Svelte Source (This page)

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(`&#128128; 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(`&#128640; 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
							`&#128128; 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
							`&#9989; ${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

$ git rev-parse --short HEAD
1597778