Skip to content

Commit 2e06424

Browse files
committed
fix: harden release-critical workflows
1 parent c55f114 commit 2e06424

16 files changed

Lines changed: 253 additions & 311 deletions

frontend/src/lib/composables/createAsyncLoader.svelte.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,21 @@ export function createAsyncLoader(fetchFn) {
77
let data = $state([]);
88
let loading = $state(false);
99
let error = $state(null);
10+
let disposed = false;
1011

1112
async function load() {
12-
if (loading) return;
13+
if (loading || disposed) return;
1314

1415
loading = true;
1516
error = null;
1617

1718
try {
18-
data = (await fetchFn()) || [];
19+
const result = await fetchFn();
20+
if (disposed) return;
21+
data = result || [];
1922
} catch (e) {
20-
// Navigation and superseded requests abort in-flight fetches by design.
21-
// The owning component is either gone or about to load newer data, so an
22-
// AbortError is neither a user-visible loader error nor a console error.
23-
if (e?.name === 'AbortError') return;
23+
// Navigation can destroy the owner or abort its in-flight request.
24+
if (disposed || e?.name === 'AbortError') return;
2425
console.error('Failed to load data:', e);
2526
error = e.message || 'Failed to load data';
2627
data = [];
@@ -34,6 +35,10 @@ export function createAsyncLoader(fetchFn) {
3435
await load();
3536
}
3637

38+
function dispose() {
39+
disposed = true;
40+
}
41+
3742
return {
3843
get data() {
3944
return data;
@@ -46,5 +51,6 @@ export function createAsyncLoader(fetchFn) {
4651
},
4752
load,
4853
refetch,
54+
dispose,
4955
};
5056
}

frontend/src/lib/pickers/AssetPicker.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
// pagination, a column change, or navigation while its request was active.
3939
onDestroy(() => {
4040
externalLoadToken += 1;
41+
assets.dispose();
4142
});
4243
4344
const assets = createAsyncLoader(async () => {

frontend/src/lib/pickers/CustomerOrganisationPicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { Building2 } from '@lucide/svelte';
44
import { createAsyncLoader } from '../composables';
55
import { api } from '../api.js';
6-
import { onMount } from 'svelte';
6+
import { onDestroy, onMount } from 'svelte';
77
88
let {
99
value = $bindable(null),
@@ -21,6 +21,7 @@
2121
2222
const organisations = createAsyncLoader(() => api.customerOrganisations.getAll());
2323
onMount(() => { if (providedOrganisations === null) organisations.load(); });
24+
onDestroy(() => organisations.dispose());
2425
const organisationOptions = $derived(providedOrganisations ?? organisations.data ?? []);
2526
</script>
2627

frontend/src/lib/pickers/DocumentPicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { BasePicker } from '.';
33
import { createAsyncLoader } from '../composables';
44
import { api } from '../api.js';
5-
import { untrack } from 'svelte';
5+
import { onDestroy, untrack } from 'svelte';
66
import { FileText } from '@lucide/svelte';
77
import { t } from '../stores/i18n.svelte.js';
88
@@ -32,6 +32,7 @@
3232
const result = await api.logbook.listDocuments(bucketId, { limit: 50 });
3333
return result?.data ?? (Array.isArray(result) ? result : []);
3434
});
35+
onDestroy(() => documents.dispose());
3536
3637
// Reload when bucketId or searchQuery changes
3738
$effect(() => {

frontend/src/lib/pickers/GroupPicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { BasePicker } from '.';
33
import { createAsyncLoader } from '../composables';
44
import { api } from '../api.js';
5-
import { onMount } from 'svelte';
5+
import { onDestroy, onMount } from 'svelte';
66
import { Users } from '@lucide/svelte';
77
import { t } from '../stores/i18n.svelte.js';
88
@@ -21,6 +21,7 @@
2121
const groups = createAsyncLoader(() => api.get('/groups'));
2222
2323
onMount(() => groups.load());
24+
onDestroy(() => groups.dispose());
2425
</script>
2526

2627
<BasePicker

frontend/src/lib/pickers/PagePicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { createAsyncLoader } from '../composables';
44
import { api } from '../api.js';
55
import { FileText } from '@lucide/svelte';
6-
import { untrack } from 'svelte';
6+
import { onDestroy, untrack } from 'svelte';
77
import { t } from '../stores/i18n.svelte.js';
88
99
let {
@@ -33,6 +33,7 @@
3333
const items = result?.results ?? (Array.isArray(result) ? result : []);
3434
return items;
3535
});
36+
onDestroy(() => pages.dispose());
3637
3738
$effect(() => {
3839
if (workspaceId) {

frontend/src/lib/pickers/PortalCustomerPicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { User } from '@lucide/svelte';
44
import { createAsyncLoader } from '../composables';
55
import { api } from '../api.js';
6-
import { onMount } from 'svelte';
6+
import { onDestroy, onMount } from 'svelte';
77
88
let {
99
value = $bindable(null),
@@ -21,6 +21,7 @@
2121
2222
const customers = createAsyncLoader(() => api.portalCustomers.getAll());
2323
onMount(() => { if (providedCustomers === null) customers.load(); });
24+
onDestroy(() => customers.dispose());
2425
const customerOptions = $derived(providedCustomers ?? customers.data ?? []);
2526
</script>
2627

frontend/src/lib/pickers/RolePicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { BasePicker } from '.';
33
import { createAsyncLoader } from '../composables';
44
import { api } from '../api.js';
5-
import { onMount } from 'svelte';
5+
import { onDestroy, onMount } from 'svelte';
66
import { Shield } from '@lucide/svelte';
77
import { t } from '../stores/i18n.svelte.js';
88
@@ -24,6 +24,7 @@
2424
const roles = createAsyncLoader(() => api.get('/workspace-roles'));
2525
2626
onMount(() => roles.load());
27+
onDestroy(() => roles.dispose());
2728
</script>
2829

2930
<BasePicker

frontend/src/lib/pickers/UserPicker.svelte

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { BasePicker } from '.';
44
import { createAsyncLoader } from '../composables';
55
import { api } from '../api.js';
6-
import { onMount } from 'svelte';
6+
import { onDestroy, onMount } from 'svelte';
77
import Avatar from '../components/Avatar.svelte';
88
import Text from '../components/Text.svelte';
99
import { t } from '../stores/i18n.svelte.js';
@@ -37,6 +37,7 @@
3737
workspaceId ? api.getAssignableUsers(workspaceId) : api.getUsers()
3838
);
3939
onMount(() => { if (!users) loader.load(); });
40+
onDestroy(() => loader.dispose());
4041
4142
let usersList = $derived(users ?? loader.data ?? []);
4243

0 commit comments

Comments
 (0)