fix(13-02): eliminate all ESLint violations — pnpm lint exits 0

- eslint.config.js: disable React Compiler rules (v7 flat.recommended enables
  them; codebase does not use the Compiler); add e2e/ to disableTypeChecked
  block; promote exhaustive-deps to error
- API broker: remove redundant as-casts (outboxWorker, poller, reminderScheduler,
  expand, sync, vevent, spike); add targeted ical.js no-unsafe-assignment/argument
  disables with justifying comments inside try blocks
- API routes/sse.ts: fix no-misused-promises on async writeSSE callback with
  void+IIFE+catch pattern
- API routes/lists.ts: let → const for updateValues
- API tests: remove unused imports (beforeEach, eq, vi); rename unused vars
  with _ prefix; remove unused lastActiveId assignment
- PWA components: void navigate() and void queryClient.invalidateQueries() on
  all fire-and-forget call sites; fix CalendarShell explicit-type-casts;
  Couldn't → HTML entity
- PWA test files: as unknown as Response for partial mock objects; string | null
  type annotation on mockLastSyncedUid; remove async from test callbacks without
  await; act(() => {}) not await act(async () => {}) for sync ops
- sw.ts: restructure Notification.data?.url access as let+if so disable
  comments land on the exact violation lines; void self.skipWaiting()
This commit is contained in:
Lucas Berger
2026-06-11 20:23:38 -04:00
parent 39e26561cf
commit 03e953158a
31 changed files with 176 additions and 121 deletions
+34 -33
View File
@@ -175,8 +175,8 @@ describe('createEvent', () => {
const mockFetch = vi.mocked(fetch)
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'test-uid-123' }),
} as Response)
json: () => ({ uid: 'test-uid-123' }),
} as unknown as Response)
const { createEvent } = await import('./client.js')
const payload = {
@@ -193,6 +193,7 @@ describe('createEvent', () => {
expect.objectContaining({
method: 'POST',
credentials: 'include',
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expect.objectContaining returns 'any' (Vitest asymmetric matcher); safe in assertion context
headers: expect.objectContaining({ 'Content-Type': 'application/json' }),
}),
)
@@ -201,8 +202,8 @@ describe('createEvent', () => {
it('returns { uid } from 202 response', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'returned-uid-456' }),
} as Response)
json: () => ({ uid: 'returned-uid-456' }),
} as unknown as Response)
const { createEvent } = await import('./client.js')
const result = await createEvent({
@@ -220,8 +221,8 @@ describe('createEvent', () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 400,
json: async () => ({ error: 'Bad Request' }),
} as Response)
json: () => ({ error: 'Bad Request' }),
} as unknown as Response)
const { createEvent } = await import('./client.js')
await expect(
@@ -250,8 +251,8 @@ describe('updateEvent', () => {
const mockFetch = vi.mocked(fetch)
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'edit-uid-789' }),
} as Response)
json: () => ({ uid: 'edit-uid-789' }),
} as unknown as Response)
const { updateEvent } = await import('./client.js')
await updateEvent('edit-uid-789', {
@@ -274,8 +275,8 @@ describe('updateEvent', () => {
it('returns { uid } on success', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'patched-uid' }),
} as Response)
json: () => ({ uid: 'patched-uid' }),
} as unknown as Response)
const { updateEvent } = await import('./client.js')
const result = await updateEvent('some-uid', {
@@ -303,12 +304,12 @@ describe('fetchWritableCalendars', () => {
it('GETs /api/events/writable-calendars with credentials:include', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({
json: () => ({
calendars: [
{ url: 'https://caldav.fastmail.com/cal1', displayName: 'My Calendar', color: '#4A90D9', isShared: false },
],
}),
} as Response)
} as unknown as Response)
const { fetchWritableCalendars } = await import('./client.js')
await fetchWritableCalendars()
@@ -326,8 +327,8 @@ describe('fetchWritableCalendars', () => {
]
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ calendars: mockCalendars }),
} as Response)
json: () => ({ calendars: mockCalendars }),
} as unknown as Response)
const { fetchWritableCalendars } = await import('./client.js')
const result = await fetchWritableCalendars()
@@ -340,7 +341,7 @@ describe('fetchWritableCalendars', () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 401,
} as Response)
} as unknown as Response)
const { fetchWritableCalendars } = await import('./client.js')
await expect(fetchWritableCalendars()).rejects.toThrow()
@@ -361,8 +362,8 @@ describe('deleteEvent', () => {
const mockFetch = vi.mocked(fetch)
mockFetch.mockResolvedValueOnce({
ok: true,
json: async () => ({}),
} as Response)
json: () => ({}),
} as unknown as Response)
const { deleteEvent } = await import('./client.js')
await deleteEvent('uid-to-delete')
@@ -379,8 +380,8 @@ describe('deleteEvent', () => {
it('resolves void on success (204/202)', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({}),
} as Response)
json: () => ({}),
} as unknown as Response)
const { deleteEvent } = await import('./client.js')
const result = await deleteEvent('uid-abc')
@@ -391,7 +392,7 @@ describe('deleteEvent', () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 404,
} as Response)
} as unknown as Response)
const { deleteEvent } = await import('./client.js')
await expect(deleteEvent('missing-uid')).rejects.toThrow()
@@ -411,8 +412,8 @@ describe('fetchSyncStatus', () => {
it('GETs /api/events/sync-status?uid= with credentials:include', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'my-uid', status: 'pending' }),
} as Response)
json: () => ({ uid: 'my-uid', status: 'pending' }),
} as unknown as Response)
const { fetchSyncStatus } = await import('./client.js')
await fetchSyncStatus('my-uid')
@@ -426,8 +427,8 @@ describe('fetchSyncStatus', () => {
it('returns SyncStatus object with uid and status', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'test-uid', status: 'done' }),
} as Response)
json: () => ({ uid: 'test-uid', status: 'done' }),
} as unknown as Response)
const { fetchSyncStatus } = await import('./client.js')
const result = await fetchSyncStatus('test-uid')
@@ -437,8 +438,8 @@ describe('fetchSyncStatus', () => {
it('returns SyncStatus with error field for failed status', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: async () => ({ uid: 'fail-uid', status: 'failed', error: '412 Conflict' }),
} as Response)
json: () => ({ uid: 'fail-uid', status: 'failed', error: '412 Conflict' }),
} as unknown as Response)
const { fetchSyncStatus } = await import('./client.js')
const result = await fetchSyncStatus('fail-uid')
@@ -449,7 +450,7 @@ describe('fetchSyncStatus', () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 500,
} as Response)
} as unknown as Response)
const { fetchSyncStatus } = await import('./client.js')
await expect(fetchSyncStatus('any-uid')).rejects.toThrow()
@@ -471,8 +472,8 @@ describe('fetchMe', () => {
ok: true,
type: 'basic',
status: 200,
json: async () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }),
} as Response)
json: () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }),
} as unknown as Response)
const { fetchMe } = await import('./client.js')
await fetchMe()
@@ -488,8 +489,8 @@ describe('fetchMe', () => {
ok: true,
type: 'basic',
status: 200,
json: async () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }),
} as Response)
json: () => ({ user: { id: 2, displayName: 'Me', color: '#E8734A' } }),
} as unknown as Response)
const { fetchMe } = await import('./client.js')
const result = await fetchMe()
@@ -503,7 +504,7 @@ describe('fetchMe', () => {
ok: false,
type: 'opaqueredirect',
status: 0,
json: async () => {
json: () => {
throw new Error('body not accessible on opaqueredirect')
},
} as unknown as Response)
@@ -517,7 +518,7 @@ describe('fetchMe', () => {
ok: false,
type: 'basic',
status: 401,
} as Response)
} as unknown as Response)
const { fetchMe } = await import('./client.js')
await expect(fetchMe()).rejects.toThrow(/authentication required/i)
+1 -1
View File
@@ -12,7 +12,7 @@
import React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { render, screen } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
// ── Module mocks ────────────────────────────────────────────────────────────
+5 -5
View File
@@ -156,7 +156,7 @@ export function CalendarShell() {
// IANATimezone (the config's timezone type) is declared but not exported by @schedule-x/calendar,
// so derive it from useCalendarApp's config parameter rather than importing it.
type SxTimeZone = NonNullable<Parameters<typeof useCalendarApp>[0]['timezone']>
const displayTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone as SxTimeZone
const displayTimeZone: SxTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
// useCalendarApp — config is stable; plugins passed as second argument
const calendar = useCalendarApp(
@@ -169,7 +169,7 @@ export function CalendarShell() {
],
defaultView,
timezone: displayTimeZone,
firstDayOfWeek: SX_FIRST_DAY_OF_WEEK as 7,
firstDayOfWeek: SX_FIRST_DAY_OF_WEEK,
calendars: calendarsConfig,
callbacks: {
onRangeUpdate(range) {
@@ -197,7 +197,7 @@ export function CalendarShell() {
useEffect(() => {
if (!eventsQuery.data) return
const sxEvents = hydrateEvents(eventsQuery.data.occurrences)
eventsService.set(sxEvents as Parameters<typeof eventsService.set>[0])
eventsService.set(sxEvents)
}, [eventsQuery.data, eventsService])
// Auth redirect — one-shot full-page nav to /api/login when /api/me fails.
@@ -351,7 +351,7 @@ export function CalendarShell() {
color: 'var(--color-text-primary)',
}}
>
Couldn't load events
Couldn&apos;t load events
</h2>
<p
style={{
@@ -363,7 +363,7 @@ export function CalendarShell() {
Check your connection and try again.
</p>
<button
onClick={() => queryClient.refetchQueries({ queryKey: ['events'] })}
onClick={() => { void queryClient.refetchQueries({ queryKey: ['events'] }) }}
style={{
background: 'var(--color-surface-dim)',
border: '1px solid var(--color-border)',
+4 -4
View File
@@ -22,7 +22,7 @@
import { useEffect, useRef, useState } from 'react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { createList } from '../api/listsClient.js'
import type { List, ListsResponse } from '../api/listsClient.js'
import type { ListsResponse } from '../api/listsClient.js'
import { useListsStore } from '../store/listsStore.js'
export function CreateListSheet() {
@@ -82,7 +82,7 @@ export function CreateListSheet() {
ownerId: 0, // unknown until response
activeCount: 0,
doneCount: 0,
} as List,
},
],
}))
return { previous }
@@ -94,8 +94,8 @@ export function CreateListSheet() {
}
},
onSettled: () => {
// Always invalidate to get canonical server state
queryClient.invalidateQueries({ queryKey: ['lists'] })
// Always invalidate to get canonical server state; fire-and-forget (React Query handles cache update)
void queryClient.invalidateQueries({ queryKey: ['lists'] })
},
onSuccess: () => {
handleClose()
@@ -33,7 +33,7 @@ const {
mockSetLastSyncedUid: vi.fn(),
mockSetOpenEventId: vi.fn(),
mockDeleteDialogOpen: { value: true },
mockDeleteDialogUid: { value: 'event-uid-to-delete' as string | null },
mockDeleteDialogUid: { value: 'event-uid-to-delete' },
}))
vi.mock('../store/calendarStore.js', () => ({
+7 -5
View File
@@ -332,7 +332,7 @@ describe('EventForm', () => {
const tomorrow = new Date(today)
tomorrow.setDate(today.getDate() + 1)
const todayStr = today.toISOString().slice(0, 10)
const yesterdayStr = new Date(today.setDate(today.getDate() - 1)).toISOString().slice(0, 10)
const _yesterdayStr = new Date(today.setDate(today.getDate() - 1)).toISOString().slice(0, 10)
if (dateInputs.length >= 2) {
fireEvent.change(dateInputs[0], { target: { value: todayStr } })
@@ -462,7 +462,7 @@ describe('EventForm', () => {
eventOccurrence: EDIT_OCCURRENCE,
})
const titleInput = screen.getByPlaceholderText('Event title') as HTMLInputElement
const titleInput = screen.getByPlaceholderText<HTMLInputElement>('Event title')
expect(titleInput.value).toBe('Existing Meeting')
})
})
@@ -560,7 +560,7 @@ describe('EventForm — Plan 03-12 gap closures', () => {
)
// Title should be empty (occurrence not yet in cache)
const titleInput = screen.getByPlaceholderText('Event title') as HTMLInputElement
const titleInput = screen.getByPlaceholderText<HTMLInputElement>('Event title')
expect(titleInput.value).toBe('')
// Phase 2: occurrence arrives in cache (simulating TanStack Query resolving)
@@ -576,7 +576,7 @@ describe('EventForm — Plan 03-12 gap closures', () => {
// WR-03 fix: the reset effect must re-run because occurrence changed,
// so the title should now be populated
await waitFor(() => {
const titleInputAfter = screen.getByPlaceholderText('Event title') as HTMLInputElement
const titleInputAfter = screen.getByPlaceholderText<HTMLInputElement>('Event title')
expect(titleInputAfter.value).toBe('Late-Arriving Meeting')
})
})
@@ -676,7 +676,7 @@ describe('EventForm — Plan 03-12 gap closures', () => {
it('IN-03: todayIso is exported from calendarStore (single source of truth)', async () => {
// Use importActual to bypass the vi.mock() and test the real module export
const actualModule = await vi.importActual('../store/calendarStore.js') as Record<string, unknown>
const actualModule = await vi.importActual('../store/calendarStore.js')
expect(typeof actualModule.todayIso).toBe('function')
const result = (actualModule.todayIso as () => string)()
// Should return a YYYY-MM-DD string
@@ -950,9 +950,11 @@ describe('EventForm — Plan 06-06 end-tracking + recurrence-bound', () => {
await waitFor(() => {
expect(mockCreateEvent).toHaveBeenCalledWith(
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expect.anything() returns 'any' (Vitest asymmetric matcher); safe in assertion context
expect.not.objectContaining({ recurrenceUntil: expect.anything() }),
)
expect(mockCreateEvent).toHaveBeenCalledWith(
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- expect.anything() returns 'any' (Vitest asymmetric matcher); safe in assertion context
expect.not.objectContaining({ recurrenceCount: expect.anything() }),
)
})
+1 -1
View File
@@ -271,7 +271,7 @@ export function EventForm() {
// read it if a future API version adds it, and default to 'none' when not present
// (WR-03 v1 comment: occurrence edits whose recurrence is not in the cache default
// to 'none'; this will be addressed when the occurrence/expand contract is extended).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access -- occurrence.recurrence is not in CalendarOccurrence v1 type; cast to any to read a future API field, default 'none' when absent
const derivedRecurrence = (occurrence as any)?.recurrence as RecurrencePreset | undefined
setRecurrence(derivedRecurrence ?? 'none')
// D-06: reset bound state to defaults on form open/occurrence change
@@ -91,7 +91,7 @@ describe('useAndroidInstallPrompt', () => {
vi.clearAllMocks()
})
it('sets canInstall=true when a beforeinstallprompt event is dispatched', async () => {
it('sets canInstall=true when a beforeinstallprompt event is dispatched', () => {
const { result } = renderHook(() => useAndroidInstallPrompt())
expect(result.current.canInstall).toBe(false)
@@ -109,7 +109,7 @@ describe('useAndroidInstallPrompt', () => {
expect(result.current.canInstall).toBe(true)
})
it('sets canInstall=false when appinstalled event fires', async () => {
it('sets canInstall=false when appinstalled event fires', () => {
const { result } = renderHook(() => useAndroidInstallPrompt())
// First fire beforeinstallprompt to set canInstall=true
@@ -14,7 +14,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
vi.mock('../hooks/usePushSubscription.js', () => ({
usePushSubscription: vi.fn(() => ({
permission: 'denied' as NotificationPermission,
permission: 'denied',
isSubscribed: false,
subscribe: vi.fn(),
setEnabled: vi.fn(),
+1 -1
View File
@@ -39,7 +39,7 @@ export function ListCard({ list, onDelete }: ListCardProps) {
const [showDelete, setShowDelete] = useState(false)
const handleCardClick = () => {
navigate(`/lists/${list.id}`)
void navigate(`/lists/${list.id}`)
}
const handleDeleteClick = (e: React.MouseEvent) => {
@@ -78,7 +78,7 @@ describe('SyncStateToast', () => {
vi.useRealTimers()
})
it('renders nothing when lastSyncedUid is null', async () => {
it('renders nothing when lastSyncedUid is null', () => {
mockLastSyncedUid.value = null
const { container } = renderToast(queryClient)
expect(container.firstChild).toBeNull()
+1 -1
View File
@@ -53,7 +53,7 @@ class MockEventSource {
constructor(url: string, init?: { withCredentials?: boolean }) {
this.url = url
this.withCredentials = init?.withCredentials ?? false
mockInstances.push(this as unknown as MockEventSourceInstance)
mockInstances.push(this)
}
addEventListener(type: string, handler: (ev: MessageEvent) => void) {
+4 -3
View File
@@ -55,7 +55,8 @@ export function useListSSE({ listId, onStateChange }: UseListSSEOptions): void {
// handleListChange is stable — invalidate the list on any list-change event
const handleListChange = useCallback(() => {
queryClient.invalidateQueries({ queryKey: ['list', listId] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', listId] })
}, [queryClient, listId])
const connect = useCallback(() => {
@@ -76,8 +77,8 @@ export function useListSSE({ listId, onStateChange }: UseListSSEOptions): void {
// Reset attempt counter on successful open (D-11)
attemptsRef.current = 0
onStateChange('connected')
// Full refetch on (re)connect — D-10: no Last-Event-ID replay, just refetch
queryClient.invalidateQueries({ queryKey: ['list', listId] })
// Full refetch on (re)connect — D-10: no Last-Event-ID replay, just refetch; fire-and-forget
void queryClient.invalidateQueries({ queryKey: ['list', listId] })
}
es.onerror = () => {
+10 -10
View File
@@ -11,8 +11,8 @@
* Uses React Query's QueryClient directly (no mocked server).
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act } from '@testing-library/react'
import { describe, it, expect, beforeEach } from 'vitest'
import { act } from '@testing-library/react'
import { QueryClient } from '@tanstack/react-query'
import type { ListItem, ListItemsResponse } from '../api/listsClient.js'
@@ -80,7 +80,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
expect(updated?.items[0].checked).toBe(false)
})
it('rolls back the checked state if the server PATCH returns an error', async () => {
it('rolls back the checked state if the server PATCH returns an error', () => {
const item = makeItem({ checked: false })
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([item]))
@@ -88,7 +88,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
const previous = queryClient.getQueryData<ListItemsResponse>(['list', LIST_ID])
// Step 2: apply optimistic update
await act(async () => {
act(() => {
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
items: (old?.items ?? []).map((i) =>
i.id === item.id ? { ...i, checked: true } : i,
@@ -101,7 +101,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
expect(after?.items[0].checked).toBe(true)
// Step 3: simulate onError rollback
await act(async () => {
act(() => {
if (previous) {
queryClient.setQueryData(['list', LIST_ID], previous)
}
@@ -112,7 +112,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
expect(rolledBack?.items[0].checked).toBe(false)
})
it('adding an item shows it in the list immediately (optimistic insert)', async () => {
it('adding an item shows it in the list immediately (optimistic insert)', () => {
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], makeItemsResponse([]))
const optimisticItem: ListItem = {
@@ -123,7 +123,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
rank: 'a0',
}
await act(async () => {
act(() => {
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
items: [...(old?.items ?? []), optimisticItem],
}))
@@ -136,7 +136,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
expect(data?.items[0].id).toBeLessThan(0)
})
it('removes the optimistically-added item if the server POST returns an error', async () => {
it('removes the optimistically-added item if the server POST returns an error', () => {
const existingItem = makeItem({ id: 1 })
queryClient.setQueryData<ListItemsResponse>(
['list', LIST_ID],
@@ -155,7 +155,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
rank: 'a1',
}
await act(async () => {
act(() => {
queryClient.setQueryData<ListItemsResponse>(['list', LIST_ID], (old) => ({
items: [...(old?.items ?? []), optimisticItem],
}))
@@ -166,7 +166,7 @@ describe('ListDetail — D-07 optimistic update + rollback', () => {
).toHaveLength(2)
// Simulate rollback on error
await act(async () => {
act(() => {
if (previous) queryClient.setQueryData(['list', LIST_ID], previous)
})
+9 -5
View File
@@ -181,7 +181,8 @@ export function ListDetail() {
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
},
})
@@ -207,7 +208,8 @@ export function ListDetail() {
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
},
})
@@ -223,7 +225,8 @@ export function ListDetail() {
},
// No onError rollback — delete-wins (D-09)
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
},
})
@@ -251,7 +254,8 @@ export function ListDetail() {
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
},
})
@@ -351,7 +355,7 @@ export function ListDetail() {
}}
>
<button
onClick={() => navigate('/lists')}
onClick={() => { void navigate('/lists') }}
aria-label="Back to lists"
style={{
background: 'none',
+4 -3
View File
@@ -66,10 +66,11 @@ export function ListsIndex() {
// TODO: surface "Couldn't delete. Try again." toast (Plan 06 / notification layer)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['lists'] })
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['lists'] })
},
onSuccess: () => {
navigate('/lists')
void navigate('/lists')
setDeleteTarget(null)
},
})
@@ -195,7 +196,7 @@ export function ListsIndex() {
</div>
<button
type="button"
onClick={() => refetch()}
onClick={() => { void refetch() }}
style={{
background: 'var(--color-member-0)',
color: '#fff',
+12 -7
View File
@@ -35,7 +35,8 @@ declare const self: ServiceWorkerGlobalScope
// AutoUpdate behavior: replace old SW immediately on install/activate.
// Equivalent to the former generateSW autoUpdate: 'prompt' → 'autoUpdate' path.
// ---------------------------------------------------------------------------
self.skipWaiting()
// skipWaiting() resolves when the SW is installed; fire-and-forget is the correct pattern here
void self.skipWaiting()
clientsClaim()
// ---------------------------------------------------------------------------
@@ -156,10 +157,14 @@ self.addEventListener('push', (event: PushEvent) => {
self.addEventListener('notificationclick', (event: NotificationEvent) => {
event.notification.close()
const url: string =
typeof event.notification.data?.url === 'string'
? event.notification.data.url
: '/'
// Notification.data is typed as 'any' in the ServiceWorker lib; we validate with typeof
// before using the value so the access is safe despite the lack of static types.
let url = '/'
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Notification.data is 'any' per webworker lib; typeof guard on the right-hand side validates this access
if (typeof event.notification.data?.url === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access -- Notification.data is 'any'; typeof check above validates this access
url = event.notification.data.url as string
}
event.waitUntil(
self.clients
@@ -170,8 +175,8 @@ self.addEventListener('notificationclick', (event: NotificationEvent) => {
// from the current window location (query string with event uid / date).
for (const client of clientList) {
if ('focus' in client) {
return (client as WindowClient).focus().then(() =>
(client as WindowClient).navigate(url)
return client.focus().then(() =>
client.navigate(url)
)
}
}