Skip to content

Commit e3171f9

Browse files
committed
Speed up bang redirects on the hot path.
Minify the service worker, reuse the inlined hot map without cloning when possible, and resolve common queries synchronously so SPA/Edge/SW spend less time before the 302.
1 parent 703d221 commit e3171f9

8 files changed

Lines changed: 232 additions & 91 deletions

File tree

api/go.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,19 @@
44
*/
55

66
import { inflateBangs } from "../shared/bang-compact";
7-
import { HOT_BANGS } from "../shared/bangs-hot.generated";
8-
import { searxSearchTemplate } from "../shared/searx";
7+
import { cookieValue } from "../shared/cookie";
8+
import { INLINE_HOT_MAP, withPrefsOverlays } from "../shared/hot-redirect";
99
import { normalizeBangPrefix } from "../shared/share-prefs";
10-
import {
11-
buildBangMap,
12-
ensureEssentialBangs,
13-
resolveBangRedirectUrl,
14-
type Bang,
15-
} from "../src/redirect";
10+
import { resolveBangRedirectUrl, type Bang } from "../src/redirect";
1611

1712
export const config = {
1813
runtime: "edge",
1914
};
2015

21-
const HOT_MAP = ensureEssentialBangs(
22-
buildBangMap(inflateBangs([...HOT_BANGS])),
23-
);
24-
25-
function cookieValue(
26-
header: string | null,
27-
name: string,
28-
): string | null {
29-
if (!header) return null;
30-
const match = header.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
31-
if (!match?.[1]) return null;
32-
try {
33-
return decodeURIComponent(match[1]);
34-
} catch {
35-
return match[1];
36-
}
37-
}
38-
3916
function parseCustomBangs(raw: string | null): Bang[] {
4017
if (!raw) return [];
4118
try {
42-
const data = JSON.parse(raw) as unknown;
19+
const data: unknown = JSON.parse(raw);
4320
if (!Array.isArray(data)) return [];
4421
return inflateBangs(
4522
data.slice(0, 40).map((item) => {
@@ -86,18 +63,10 @@ export default async function handler(request: Request): Promise<Response> {
8663
const searxHost = cookieValue(cookie, "searx-instance") ?? "";
8764
const custom = parseCustomBangs(cookieValue(cookie, "custom-bangs"));
8865

89-
const map = ensureEssentialBangs(new Map(HOT_MAP));
90-
for (const bang of custom) map.set(bang.t, bang);
91-
if (searxHost) {
92-
for (const t of ["searx", "searxng"]) {
93-
map.set(t, {
94-
t,
95-
d: searxHost,
96-
u: searxSearchTemplate(searxHost),
97-
s: "SearxNG",
98-
});
99-
}
100-
}
66+
const map = withPrefsOverlays(INLINE_HOT_MAP, {
67+
customBangs: custom,
68+
customSearxUrl: searxHost,
69+
});
10170

10271
const target = resolveBangRedirectUrl(q, map, defaultBang, { bangPrefix });
10372
if (!target) return serveSpa(request);

shared/cookie.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Fast cookie helpers — avoid allocating a RegExp per lookup on the redirect hot path.
3+
*/
4+
5+
export function cookieValue(
6+
header: string | null | undefined,
7+
name: string,
8+
): string | null {
9+
if (!header) return null;
10+
const needle = `${name}=`;
11+
let start = 0;
12+
while (start < header.length) {
13+
const at = header.indexOf(needle, start);
14+
if (at === -1) return null;
15+
if (at !== 0 && header.charCodeAt(at - 1) !== 32 /* space */ && header.charCodeAt(at - 1) !== 59 /* ; */) {
16+
start = at + 1;
17+
continue;
18+
}
19+
const valueStart = at + needle.length;
20+
const valueEnd = header.indexOf(";", valueStart);
21+
const raw =
22+
valueEnd === -1 ? header.slice(valueStart) : header.slice(valueStart, valueEnd);
23+
try {
24+
return decodeURIComponent(raw);
25+
} catch {
26+
return raw;
27+
}
28+
}
29+
return null;
30+
}

shared/hot-redirect.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { describe, expect, it } from "vitest";
2+
import { cookieValue } from "./cookie";
3+
import {
4+
canResolveWithMap,
5+
INLINE_HOT_MAP,
6+
withPrefsOverlays,
7+
} from "./hot-redirect";
8+
9+
describe("cookieValue", () => {
10+
it("reads first and middle cookies", () => {
11+
expect(cookieValue("default-bang=g", "default-bang")).toBe("g");
12+
expect(
13+
cookieValue("a=1; default-bang=searx; b=2", "default-bang"),
14+
).toBe("searx");
15+
});
16+
17+
it("decodes URI components", () => {
18+
expect(cookieValue("x=%21", "x")).toBe("!");
19+
});
20+
21+
it("avoids substring false positives", () => {
22+
expect(cookieValue("xdefault-bang=g", "default-bang")).toBeNull();
23+
expect(cookieValue("foo=default-bang=g", "default-bang")).toBeNull();
24+
});
25+
});
26+
27+
describe("withPrefsOverlays", () => {
28+
it("returns the same Map when overlays are empty", () => {
29+
const base = new Map(INLINE_HOT_MAP);
30+
expect(withPrefsOverlays(base, {})).toBe(base);
31+
expect(withPrefsOverlays(INLINE_HOT_MAP, { customBangs: [] })).toBe(
32+
INLINE_HOT_MAP,
33+
);
34+
});
35+
36+
it("clones when custom bangs are present", () => {
37+
const custom = [
38+
{
39+
t: "mycustom",
40+
u: "https://example.com/?q={{{s}}}",
41+
d: "example.com",
42+
},
43+
];
44+
const next = withPrefsOverlays(INLINE_HOT_MAP, { customBangs: custom });
45+
expect(next).not.toBe(INLINE_HOT_MAP);
46+
expect(next.get("mycustom")?.d).toBe("example.com");
47+
expect(INLINE_HOT_MAP.has("mycustom")).toBe(false);
48+
});
49+
});
50+
51+
describe("canResolveWithMap", () => {
52+
it("allows default searches and hot bangs", () => {
53+
expect(canResolveWithMap("kočky", INLINE_HOT_MAP)).toBe(true);
54+
expect(canResolveWithMap("!g cats", INLINE_HOT_MAP)).toBe(true);
55+
expect(canResolveWithMap("cats !g", INLINE_HOT_MAP)).toBe(true);
56+
});
57+
58+
it("rejects rare bangs missing from hot", () => {
59+
expect(canResolveWithMap("!zzznomatchxyz hello", INLINE_HOT_MAP)).toBe(
60+
false,
61+
);
62+
});
63+
});

shared/hot-redirect.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Shared redirect hot path: inlined top bangs + zero-copy overlays when possible.
3+
*/
4+
import { inflateBangs } from "./bang-compact";
5+
import { HOT_BANGS } from "./bangs-hot.generated";
6+
import { searxSearchTemplate } from "./searx";
7+
import {
8+
buildBangMap,
9+
ensureEssentialBangs,
10+
extractBangTrigger,
11+
longestTriggerPrefix,
12+
type Bang,
13+
} from "../src/redirect";
14+
15+
/** Built once at module load — never mutate. */
16+
export const INLINE_HOT_MAP: ReadonlyMap<string, Bang> = ensureEssentialBangs(
17+
buildBangMap(inflateBangs([...HOT_BANGS])),
18+
);
19+
20+
export type HotOverlayPrefs = {
21+
customBangs?: readonly Bang[];
22+
customSearxUrl?: string;
23+
};
24+
25+
/**
26+
* Apply custom bangs / Searx host on top of a base map.
27+
* Returns the same Map instance when overlays are empty (zero clone).
28+
*/
29+
export function withPrefsOverlays(
30+
base: ReadonlyMap<string, Bang>,
31+
prefs: HotOverlayPrefs,
32+
): Map<string, Bang> {
33+
const customs = prefs.customBangs;
34+
const host = prefs.customSearxUrl?.trim() ?? "";
35+
const hasCustoms = Boolean(customs && customs.length > 0);
36+
37+
let needsSearx = false;
38+
if (host) {
39+
const template = searxSearchTemplate(host);
40+
const cur = base.get("searxng") ?? base.get("searx");
41+
needsSearx = !cur || cur.u !== template;
42+
}
43+
44+
if (!hasCustoms && !needsSearx) {
45+
return base instanceof Map ? base : new Map(base);
46+
}
47+
48+
const map = ensureEssentialBangs(new Map(base));
49+
if (customs) {
50+
for (const bang of customs) map.set(bang.t, bang);
51+
}
52+
if (needsSearx && host) {
53+
const u = searxSearchTemplate(host);
54+
for (const t of ["searx", "searxng"] as const) {
55+
map.set(t, { t, d: host, u, s: "SearxNG" });
56+
}
57+
}
58+
return map;
59+
}
60+
61+
/**
62+
* True when `query` can be resolved correctly from `map` alone
63+
* (no need to await the full catalog for a rare bang).
64+
*/
65+
export function canResolveWithMap(
66+
query: string,
67+
map: ReadonlyMap<string, Bang>,
68+
bangPrefix = "!",
69+
): boolean {
70+
const hint = extractBangTrigger(query, bangPrefix);
71+
if (!hint) return true;
72+
if (map.has(hint)) return true;
73+
if (longestTriggerPrefix(hint, map as Map<string, Bang>)) return true;
74+
return false;
75+
}

src/main.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ import {
1919
saveCustomBangs,
2020
validateBangInput,
2121
} from "./custom-bangs";
22+
import {
23+
canResolveWithMap,
24+
withPrefsOverlays,
25+
INLINE_HOT_MAP,
26+
} from "../shared/hot-redirect";
2227
import {
2328
type Bang,
2429
ensureEssentialBangs,
@@ -1269,6 +1274,26 @@ function renderLanding(
12691274
}
12701275
}
12711276

1277+
/** Sync redirect from inlined hot map — no catalog await (SPA fallback). */
1278+
function trySyncHotRedirect(query: string): string | null {
1279+
const bangPrefix = getBangPrefix();
1280+
const map = withPrefsOverlays(INLINE_HOT_MAP, {
1281+
customBangs: loadCustomBangs(),
1282+
customSearxUrl: getSearxInstanceHost(),
1283+
});
1284+
if (!canResolveWithMap(query, map, bangPrefix)) return null;
1285+
const url = resolveBangRedirectUrl(
1286+
query,
1287+
map,
1288+
getDefaultBangTrigger(),
1289+
resolveOptions(),
1290+
);
1291+
if (!url) return null;
1292+
// Own a copy — never expose the shared INLINE_HOT_MAP to later SPA mutations.
1293+
bangMap = map === INLINE_HOT_MAP ? new Map(map) : map;
1294+
return url;
1295+
}
1296+
12721297
async function boot() {
12731298
const app = document.querySelector<HTMLDivElement>("#app")!;
12741299
const query =
@@ -1279,6 +1304,18 @@ async function boot() {
12791304
clearShareHash();
12801305
}
12811306

1307+
// Fast path: common bangs / default search without waiting on IDB/network.
1308+
if (query) {
1309+
const syncUrl = trySyncHotRedirect(query);
1310+
if (syncUrl) {
1311+
void syncPrefsToIdb();
1312+
pushHistory(query);
1313+
void recordBangUsage(usageTriggerForQuery(query));
1314+
window.location.replace(syncUrl);
1315+
return;
1316+
}
1317+
}
1318+
12821319
let baseMap: Map<string, Bang>;
12831320
try {
12841321
if (!query) {

src/redirect.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ function bangDomain(bang: Bang): string {
9090
/** Longest trigger in `bangMap` that is a prefix of `token` (case-insensitive). */
9191
export function longestTriggerPrefix(
9292
token: string,
93-
bangMap: Map<string, Bang>,
93+
bangMap: ReadonlyMap<string, Bang>,
9494
): string | null {
9595
const lower = token.toLowerCase();
9696
let best: string | null = null;

0 commit comments

Comments
 (0)