A fast, local-first "redirection engine" for !bang users with a few extra features ^-^
1import { bangs } from "./hashbang";
2import { writeFileSync } from "fs";
3import { Worker, isMainThread, parentPort, workerData } from "worker_threads";
4import { cpus } from "os";
5import dns from "dns";
6import util from "util";
7
8const TEST_QUERY = "test query";
9const TIMEOUT = 5000;
10const BATCH_SIZE = 10;
11
12const dnsCache = new Map();
13const lookup = util.promisify(dns.lookup);
14const urlCache = new Map<string, boolean>();
15
16const resolveHost = async (hostname: string) => {
17 if (dnsCache.has(hostname)) {
18 return dnsCache.get(hostname);
19 }
20 const address = await lookup(hostname);
21 dnsCache.set(hostname, address);
22 return address;
23};
24
25const testUrl = async (url: string, retries = 3): Promise<boolean> => {
26 if (urlCache.has(url)) {
27 return urlCache.get(url)!;
28 }
29
30 for (let i = 0; i < retries; i++) {
31 try {
32 const hostname = new URL(url).hostname;
33 await resolveHost(hostname);
34
35 const res = await fetch(url, {
36 signal: AbortSignal.timeout(TIMEOUT),
37 headers: { "User-Agent": "Mozilla/5.0" },
38 });
39 const result = res.status === 200;
40 urlCache.set(url, result);
41 return result;
42 } catch (err) {
43 if (i === retries - 1) {
44 urlCache.set(url, false);
45 return false;
46 }
47 await new Promise((resolve) => setTimeout(resolve, 1000 * i));
48 }
49 }
50 return false;
51};
52
53if (isMainThread) {
54 const brokenBangs: { bang: string; url: string }[] = [];
55 const bangEntries = Object.entries(bangs);
56 const numThreads = cpus().length;
57 const chunkSize = Math.ceil(bangEntries.length / numThreads);
58 const chunks = Array.from({ length: numThreads }, (_, i) =>
59 bangEntries.slice(i * chunkSize, (i + 1) * chunkSize),
60 );
61
62 let completedBangs = 0;
63 const startTime = Date.now();
64 const spinChars = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
65 let spinIdx = 0;
66
67 const updateProgress = setInterval(() => {
68 const elapsedSeconds = (Date.now() - startTime) / 1000;
69 const bangsPerSecond = (completedBangs / elapsedSeconds).toFixed(2);
70 try {
71 process.stdout.clearLine(0);
72 process.stdout.cursorTo(0);
73 process.stdout.write(
74 `${spinChars[spinIdx]} Completed ${completedBangs}/${bangEntries.length} bangs (${bangsPerSecond}/s)`,
75 );
76 spinIdx = (spinIdx + 1) % spinChars.length;
77 } catch (err) {
78 process.stdout.write("\r");
79 }
80 }, 500);
81
82 const workers = chunks.map((chunk) => {
83 return new Promise((resolve) => {
84 const worker = new Worker(__filename, {
85 workerData: chunk,
86 });
87
88 worker.on("message", (msg) => {
89 if (msg.type === "progress") {
90 completedBangs++;
91 } else {
92 brokenBangs.push(...msg);
93 }
94 });
95
96 worker.on("exit", resolve);
97 });
98 });
99
100 Promise.all(workers).then(() => {
101 clearInterval(updateProgress);
102 process.stdout.write("\n");
103 const brokenOutput = JSON.stringify(brokenBangs, null, 2);
104 writeFileSync("src/bangs/broken-bangs.json", brokenOutput);
105 console.log("Broken bangs written to broken-bangs.json");
106 });
107} else {
108 const brokenBangsChunk: { bang: string; url: string }[] = [];
109 const chunk = workerData as [string, { u: string }][];
110
111 (async () => {
112 for (let i = 0; i < chunk.length; i += BATCH_SIZE) {
113 const batch = chunk.slice(i, i + BATCH_SIZE);
114 const results = await Promise.all(
115 batch.map(async ([key, bang]) => {
116 const url = bang.u.replace("{{{s}}}", encodeURIComponent(TEST_QUERY));
117 const isWorking = await testUrl(url);
118 return { key, url, isWorking };
119 }),
120 );
121
122 for (const { key, url, isWorking } of results) {
123 if (!isWorking) {
124 brokenBangsChunk.push({ bang: key, url });
125 try {
126 process.stdout.clearLine(0);
127 process.stdout.cursorTo(0);
128 } catch (err) {
129 process.stdout.write("\r");
130 }
131 console.log(`Bang ${key} is broken (${url})`);
132 }
133 parentPort?.postMessage({ type: "progress" });
134 }
135
136 if (brokenBangsChunk.length > 1000) {
137 parentPort?.postMessage(brokenBangsChunk);
138 brokenBangsChunk.length = 0;
139 }
140 }
141
142 if (brokenBangsChunk.length > 0) {
143 parentPort?.postMessage(brokenBangsChunk);
144 }
145 })();
146}