Skip to content
This repository was archived by the owner on Sep 21, 2026. It is now read-only.

Commit 26026dd

Browse files
committed
perf: prefetch project list on component mount to eliminate dropdown delay
The project list was only fetched after a row appeared, causing a ~1 second blank dropdown. Now the API call fires the instant the ProjectAccessSelector mounts (when the user switches to "Specific projects"), so data is ready or in-flight before any interaction. All rows share a single Promise for unfiltered loads via prefetchPromise; typed searches still hit the API individually. The generation-counter race guard still applies so concurrent awaits can't overwrite each other.
1 parent f113c4b commit 26026dd

1 file changed

Lines changed: 38 additions & 15 deletions

File tree

‎src/routes/(console)/organization-[organization]/projectAccessSelector.svelte‎

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { Icon, Layout, Input } from '@appwrite.io/pink-svelte';
99
import { IconPlus, IconTrash } from '@appwrite.io/pink-icons-svelte';
1010
import { debounce } from '$lib/helpers/debounce';
11+
import { onMount } from 'svelte';
1112
1213
type ProjectAccess = { projectId: string; roleName: string };
1314
type Option = { value: string; label: string };
@@ -25,6 +26,26 @@
2526
// discarded if a newer request has already been dispatched.
2627
let rowGeneration = $state<number[]>([]);
2728
29+
// Prefetch the first page the moment this component mounts (i.e. when the
30+
// user switches to "Specific projects") so results are ready or in-flight
31+
// before any row is opened. All rows share this single Promise for
32+
// unfiltered loads; typed searches always hit the API separately.
33+
let prefetchPromise: Promise<Option[]> | null = null;
34+
35+
onMount(() => {
36+
prefetchPromise = sdk.forConsole
37+
.organization($organization.$id)
38+
.listProjects({
39+
queries: [
40+
Query.limit(25),
41+
Query.orderDesc(''),
42+
Query.equal('teamId', $organization.$id)
43+
]
44+
})
45+
.then((r) => r.projects.map((p) => ({ value: p.$id, label: p.name })))
46+
.catch(() => []);
47+
});
48+
2849
function takenIds(excludeIndex: number): Set<string> {
2950
return new Set(
3051
projectAccess
@@ -39,27 +60,29 @@
3960
rowGeneration[index] = gen;
4061
rowSearching[index] = true;
4162
42-
const queries: string[] = [
43-
Query.limit(25),
44-
Query.orderDesc(''),
45-
Query.equal('teamId', $organization.$id)
46-
];
47-
if (search) {
48-
queries.push(Query.search('name', search));
63+
let allOptions: Option[];
64+
if (!search && prefetchPromise) {
65+
// Re-use the shared prefetch — avoids a redundant network request
66+
allOptions = await prefetchPromise;
67+
} else {
68+
const queries: string[] = [
69+
Query.limit(25),
70+
Query.orderDesc(''),
71+
Query.equal('teamId', $organization.$id)
72+
];
73+
if (search) queries.push(Query.search('name', search));
74+
const result = await sdk.forConsole
75+
.organization($organization.$id)
76+
.listProjects({ queries })
77+
.catch(() => null);
78+
allOptions = (result?.projects ?? []).map((p) => ({ value: p.$id, label: p.name }));
4979
}
5080
51-
const result = await sdk.forConsole
52-
.organization($organization.$id)
53-
.listProjects({ queries })
54-
.catch(() => null);
55-
5681
// Discard stale responses — a newer request has already been dispatched
5782
if (rowGeneration[index] !== gen) return;
5883
5984
const taken = takenIds(index);
60-
rowOptions[index] = (result?.projects ?? [])
61-
.filter((p) => !taken.has(p.$id))
62-
.map((p) => ({ value: p.$id, label: p.name }));
85+
rowOptions[index] = allOptions.filter((p) => !taken.has(p.value));
6386
rowSearching[index] = false;
6487
}
6588

0 commit comments

Comments
 (0)