Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kentcdodds/package-app-kit

Design tokens, PWA install/update, About/version, cache helpers, and optional realtime notes sync for Kody package apps.

src/client-cache.ts

114 lines · 3.3 KB · TypeScript
/**
 * TanStack Query + IndexedDB persistence helpers for vanilla package apps.
 * Prefer `@tanstack/query-core` (not React Query). React adapters can come later.
 *
 * These helpers are meant to be imported from a browser/client bundle (starter
 * may ship a dedicated client module). They are not Worker-safe (IndexedDB).
 */

import { QueryClient } from '@tanstack/query-core'
import { persistQueryClient } from '@tanstack/query-persist-client-core'
import { get, set, del } from 'idb-keyval'

export type IdBPersister = {
	persistClient: (client: unknown) => Promise<void>
	restoreClient: () => Promise<unknown | undefined>
	removeClient: () => Promise<void>
}

/**
 * Create an IndexedDB persister backed by `idb-keyval`.
 * Use with `createPersistedQueryClient` for offline-friendly package app clients.
 *
 * @param input.idbKey - IndexedDB key for the dehydrated client
 * @returns Persister compatible with `persistQueryClient`
 *
 * @example
 * import { createIdbPersister } from 'kody:@kentcdodds/package-app-kit/client-cache'
 * const persister = createIdbPersister({ idbKey: 'my-app-query' })
 */
export function createIdbPersister(input?: { idbKey?: string }): IdBPersister {
	const idbKey = input?.idbKey || 'pak-query-cache'
	return {
		persistClient: async (client) => {
			await set(idbKey, client)
		},
		restoreClient: async () => {
			return await get(idbKey)
		},
		removeClient: async () => {
			await del(idbKey)
		},
	}
}

/**
 * Build a QueryClient with sensible PWA defaults (long gcTime for persistence).
 *
 * @example
 * import { createPackageQueryClient } from 'kody:@kentcdodds/package-app-kit/client-cache'
 * const client = createPackageQueryClient()
 */
export function createPackageQueryClient(input?: {
	staleTimeMs?: number
	gcTimeMs?: number
}): QueryClient {
	const staleTime = input?.staleTimeMs ?? 60_000
	const gcTime = input?.gcTimeMs ?? 1000 * 60 * 60 * 24
	return new QueryClient({
		defaultOptions: {
			queries: {
				staleTime,
				gcTime,
				retry: 1,
				refetchOnWindowFocus: true,
			},
		},
	})
}

/**
 * Create a QueryClient and attach IndexedDB persistence.
 * Call once at client boot; await restore before mounting data-dependent UI when possible.
 *
 * @param input.buster - Cache buster (bump on breaking schema / major publish)
 * @param input.maxAgeMs - Max age for restored cache
 * @returns `{ queryClient, persister, restorePromise }`
 *
 * @example
 * import { createPersistedQueryClient } from 'kody:@kentcdodds/package-app-kit/client-cache'
 * const { queryClient, restorePromise } = createPersistedQueryClient({ buster: 'v1' })
 * await restorePromise
 */
export function createPersistedQueryClient(input?: {
	idbKey?: string
	buster?: string
	maxAgeMs?: number
	staleTimeMs?: number
	gcTimeMs?: number
}): {
	queryClient: QueryClient
	persister: IdBPersister
	restorePromise: Promise<unknown>
} {
	const queryClient = createPackageQueryClient({
		staleTimeMs: input?.staleTimeMs,
		gcTimeMs: input?.gcTimeMs,
	})
	const persister = createIdbPersister({ idbKey: input?.idbKey })
	const [, restorePromise] = persistQueryClient({
		queryClient,
		persister: persister as never,
		buster: input?.buster ?? '',
		maxAge: input?.maxAgeMs ?? 1000 * 60 * 60 * 24 * 7,
	})
	return { queryClient, persister, restorePromise }
}

/**
 * Client-cache helpers overview.
 *
 */

/** Primary callable export for this subpath. */
export default createPersistedQueryClient