Skip to content

Commit f2bac96

Browse files
refactor: update catalog store and table components for improved performance and metrics tracking
- worker client middle man class for handling communication between worker and catalog store. - Tracking search request duration and displaying on client
1 parent e5fef72 commit f2bac96

10 files changed

Lines changed: 170 additions & 76 deletions

File tree

src/lib/components/search/results.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,5 @@
1515
bind:query={catalog.query}
1616
type={catalog.type}
1717
onsort={(key) => catalog.toggleSort(key)}
18-
onloadrange={(offset, limit) => catalog.loadRange(offset, limit)}
18+
onloadrange={(offset, limit) => catalog.load(offset, limit)}
1919
/>
Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<script lang="ts">
2+
import Badge from "$lib/components/ui/badge.svelte";
3+
24
interface Props {
35
count: number;
46
total: number;
7+
duration: number;
58
}
69
7-
let { count, total }: Props = $props();
10+
let { count, total, duration }: Props = $props();
811
</script>
912

10-
{#if count !== total}
11-
<span>Showing {count.toLocaleString()} of {total.toLocaleString()} results</span>
12-
{:else}
13-
<span>Showing {count.toLocaleString()} results</span>
14-
{/if}
13+
<div class="flex items-center gap-2">
14+
<span>Showing {count.toLocaleString()} of {total.toLocaleString()}</span>
15+
<Badge variant="brand">{duration.toFixed(0)}ms</Badge>
16+
</div>

src/lib/components/table/table.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,16 @@
8282
/>
8383
</div>
8484

85-
<div class="w-full h-8 px-4 py-2 border-b border-neutral-900 bg-neutral-950 shrink-0 flex items-center justify-between text-xs text-neutral-500">
85+
<div class="w-full h-8 px-4 py-2 border-b border-neutral-900 bg-neutral-950 shrink-0 flex items-center justify-between text-xs text-neutral-400">
8686
<div class="flex items-center gap-2">
8787
{#if config.status === "pending"}
8888
<Spinner class="w-3 h-3 text-neutral-500" />
8989
{:else}
90-
<Showing count={config.count} total={config.total} />
90+
<Showing
91+
count={config.count}
92+
total={config.total}
93+
duration={config.duration}
94+
/>
9195
{/if}
9296
</div>
9397
</div>

src/lib/stores/catalog.svelte.ts

Lines changed: 49 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { setContext, getContext } from "svelte";
22

3-
import SearchWorker from "$lib/workers/search.worker?worker";
3+
import { SearchClient } from "$lib/workers/client";
4+
import { MetricsStore } from "$lib/stores/metrics.svelte";
5+
import { getNextSortState } from "$lib/utilities/sort";
46

57
import type { Store } from "$lib/types/store";
68
import type { CatalogType } from "$lib/types/catalog";
79
import type { Status } from "$lib/types/status";
810
import { SortStateSchema, type SortState } from "$lib/types/table";
9-
import type { WorkerToMainMessage } from "$lib/types/worker";
1011

1112
export class CatalogStore {
1213
status = $state<Status>("pending");
@@ -15,45 +16,40 @@ export class CatalogStore {
1516
results = $state<Store[]>([]);
1617
offset = $state(0);
1718

19+
metrics = new MetricsStore();
20+
1821
_query = $state("");
1922
_type = $state<CatalogType>("stores");
2023
_sort = $state<SortState>(SortStateSchema.parse({}));
2124

22-
worker: Worker | undefined;
23-
24-
private requestId = 0;
25-
private ready = false;
25+
client: SearchClient | undefined;
2626

2727
constructor() {
2828
if (typeof window !== "undefined") {
29-
this.worker = new SearchWorker();
30-
31-
this.worker.onmessage = (e: MessageEvent<WorkerToMainMessage>) => {
32-
const message = e.data;
33-
34-
if (message.type === "ready") {
35-
this.ready = true;
36-
this.count = message.count;
37-
this.total = message.count;
29+
this.client = new SearchClient({
30+
onReady: (count) => {
31+
this.count = count;
32+
this.total = count;
3833
this.search(this.query, 0, 40);
39-
} else if (message.type === "results") {
40-
if (message.id === this.requestId && message.id !== undefined) {
41-
this.results = message.results;
42-
this.count = message.total;
43-
this.offset = message.offset;
44-
this.status = "success";
34+
},
35+
onResults: (results, total, offset) => {
36+
this.results = results;
37+
this.count = total;
38+
this.offset = offset;
39+
this.status = "success";
40+
if (this.offset === 0) {
41+
this.metrics.stop();
4542
}
46-
} else if (message.type === "error") {
43+
},
44+
onError: (error) => {
4745
this.status = "error";
48-
console.error("Catalog error:", message.error);
46+
console.error("Catalog error:", error);
4947
}
50-
};
51-
52-
this.worker.onerror = () => {
53-
this.status = "error";
54-
};
48+
});
5549

56-
this.load();
50+
this.metrics.start();
51+
52+
this.init();
5753
}
5854
}
5955

@@ -63,6 +59,7 @@ export class CatalogStore {
6359

6460
set query(value: string) {
6561
this._query = value;
62+
this.metrics.start();
6663
this.search(value, 0, 40);
6764
}
6865

@@ -72,60 +69,48 @@ export class CatalogStore {
7269

7370
set type(value: CatalogType) {
7471
this._type = value;
75-
this.status = "pending";
76-
this.results = [];
77-
this.count = 0;
78-
this.total = 0;
79-
this._query = "";
80-
this._sort = SortStateSchema.parse({});
81-
this.ready = false;
82-
this.load();
72+
73+
this.reset();
74+
75+
this.metrics.restart();
76+
77+
this.init();
8378
}
8479

8580
get sort() {
8681
return this._sort;
8782
}
8883

8984
toggleSort(key: string) {
90-
if (this._sort.key === key) {
91-
if (this._sort.direction === "asc") {
92-
this._sort = { key, direction: "desc" };
93-
} else {
94-
this._sort = SortStateSchema.parse({});
95-
}
96-
} else {
97-
this._sort = { key, direction: "asc" };
98-
}
99-
85+
this._sort = getNextSortState(this._sort, key);
86+
this.metrics.start();
10087
this.search(this.query, 0, 40);
10188
}
10289

103-
load() {
104-
if (this.worker) {
105-
this.worker.postMessage({ type: "load", payload: `/${this.type}.json` });
106-
}
90+
init() {
91+
this.client?.load(`/${this.type}.json`);
10792
}
10893

10994
search(q: string, offset: number = 0, limit: number = 20) {
110-
if (this.ready && this.worker) {
111-
this.requestId++;
112-
this.worker.postMessage({
113-
type: "search",
114-
payload: q,
115-
offset,
116-
limit,
117-
sort: this._sort.key ? { ...this._sort } : undefined,
118-
id: this.requestId
119-
});
120-
}
95+
this.client?.search(q, offset, limit, this._sort);
12196
}
12297

123-
loadRange(offset: number, limit: number) {
98+
load(offset: number, limit: number) {
12499
this.search(this.query, offset, limit);
125100
}
126101

102+
reset() {
103+
this.status = "pending";
104+
this.results = [];
105+
this.count = 0;
106+
this.total = 0;
107+
108+
this._query = "";
109+
this._sort = SortStateSchema.parse({});
110+
}
111+
127112
destroy() {
128-
this.worker?.terminate();
113+
this.client?.terminate();
129114
}
130115
}
131116

src/lib/stores/metrics.svelte.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export class MetricsStore {
2+
duration = $state(0);
3+
4+
private startTime = 0;
5+
private shouldUpdate = false;
6+
7+
start() {
8+
this.startTime = performance.now();
9+
this.shouldUpdate = true;
10+
}
11+
12+
stop() {
13+
if (this.shouldUpdate) {
14+
this.duration = performance.now() - this.startTime;
15+
this.shouldUpdate = false;
16+
}
17+
}
18+
19+
reset() {
20+
this.duration = 0;
21+
this.startTime = 0;
22+
this.shouldUpdate = false;
23+
}
24+
25+
restart() {
26+
this.reset();
27+
this.start();
28+
}
29+
}
30+

src/lib/stores/virtualization.svelte.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { getRange, getSpacers } from "$lib/utilities/virtualization";
33
import { type TableConfig, TableConfigSchema } from "$lib/types/table";
44

55
export class Virtualizer {
6-
// Scroll State
76
scrollTop = $state(0);
87
viewportHeight = $state(800);
98
innerWidth = $state(1024);

src/lib/types/table.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const TableConfigSchema = z.object({
2222
columns: z.array(ColumnSchema).default([]),
2323
count: z.number().default(0),
2424
total: z.number().default(0),
25+
duration: z.number().default(0),
2526
status: StatusSchema.default("idle"),
2627
sort: SortStateSchema.default({
2728
key: "",

src/lib/utilities/catalog.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import type { Catalog } from "$lib/stores/catalog.svelte";
1+
import type { CatalogStore } from "$lib/stores/catalog.svelte";
22

33
import { getColumns } from "$lib/utilities/columns";
44

55
import type { TableConfig } from "$lib/types/table";
66

7-
export const mapConfig = (catalog: Catalog, title: string, placeholder = "Search..."): TableConfig => {
7+
export const mapConfig = (catalog: CatalogStore, title: string, placeholder = "Search..."): TableConfig => {
88
return {
99
data: catalog.results,
1010
columns: getColumns(catalog.type),
1111
count: catalog.count,
1212
total: catalog.total,
13+
duration: catalog.metrics.duration,
1314
status: catalog.status,
1415
sort: catalog.sort,
1516
title,

src/lib/utilities/sort.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,14 @@ export const compare = (a: Store, b: Store, sort: SortState): number => {
2121
return 0;
2222
};
2323

24+
export const getNextSortState = (current: SortState, key: string): SortState => {
25+
if (current.key === key) {
26+
if (current.direction === "asc") {
27+
return { key, direction: "desc" };
28+
}
29+
return { key: "", direction: "asc" };
30+
}
31+
return { key, direction: "asc" };
32+
};
33+
2434

src/lib/workers/client.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import SearchWorker from "$lib/workers/search.worker?worker";
2+
import type { WorkerToMainMessage } from "$lib/types/worker";
3+
import type { Store } from "$lib/types/store";
4+
import type { SortState } from "$lib/types/table";
5+
6+
type SearchClientCallbacks = {
7+
onReady: (count: number) => void;
8+
onResults: (results: Store[], total: number, offset: number) => void;
9+
onError: (error: string) => void;
10+
}
11+
12+
export class SearchClient {
13+
private worker: Worker;
14+
private requestId = 0;
15+
private ready = false;
16+
17+
constructor(private callbacks: SearchClientCallbacks) {
18+
this.worker = new SearchWorker();
19+
this.worker.onmessage = this.handleMessage.bind(this);
20+
this.worker.onerror = () => this.callbacks.onError("Worker error");
21+
}
22+
23+
private handleMessage(e: MessageEvent<WorkerToMainMessage>) {
24+
const message = e.data;
25+
26+
if (message.type === "ready") {
27+
this.ready = true;
28+
this.callbacks.onReady(message.count);
29+
} else if (message.type === "results") {
30+
if (message.id === this.requestId) {
31+
this.callbacks.onResults(message.results, message.total, message.offset);
32+
}
33+
} else if (message.type === "error") {
34+
this.callbacks.onError(message.error);
35+
}
36+
}
37+
38+
load(url: string) {
39+
this.ready = false;
40+
this.worker.postMessage({ type: "load", payload: url });
41+
}
42+
43+
search(query: string, offset: number, limit: number, sort: SortState) {
44+
if (!this.ready) return;
45+
46+
this.requestId++;
47+
48+
this.worker.postMessage({
49+
type: "search",
50+
payload: query,
51+
offset,
52+
limit,
53+
sort: sort.key ? { key: sort.key, direction: sort.direction } : undefined,
54+
id: this.requestId
55+
});
56+
}
57+
58+
terminate() {
59+
this.worker.terminate();
60+
}
61+
}
62+

0 commit comments

Comments
 (0)