@void-snippets/react 0.3.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/hooks/createResourceHooks.ts","../src/hooks/useAlertMessage.ts","../src/hooks/useAsyncState.ts","../src/hooks/useCallTimer.ts","../src/hooks/useModal.ts","../src/hooks/usePagination.ts"],"sourcesContent":["import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n VSAdapters,\n VSListResult,\n VSPagination,\n VSQueryParams,\n VSDefaultPaginatedResponse,\n VSDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\nconst DEFAULT_PAGINATION: VSQueryParams = {\n page: 1,\n limit: 10,\n};\n\n// ============================================================================\n// PHANTOM TYPE CONSTRAINT\n//\n// We constrain S only on the __types phantom property (readonly = covariant)\n// so ResourceService<string, ...> is always assignable to WithResourceTypes.\n// This avoids the variance issue from constraining method parameter types.\n// ============================================================================\n\ninterface WithResourceTypes {\n readonly __types: {\n id: unknown;\n base: unknown;\n detail: unknown;\n create: unknown;\n update: unknown;\n listRaw: unknown;\n singleRaw: unknown;\n };\n}\n\ntype Id<S extends WithResourceTypes> = S[\"__types\"][\"id\"];\ntype Base<S extends WithResourceTypes> = S[\"__types\"][\"base\"];\ntype Detail<S extends WithResourceTypes> = S[\"__types\"][\"detail\"];\ntype Create<S extends WithResourceTypes> = S[\"__types\"][\"create\"];\ntype Update<S extends WithResourceTypes> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends WithResourceTypes> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends WithResourceTypes> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPES\n// ============================================================================\n\nexport interface VSUseListReturn<TBase> {\n list: TBase[];\n pagination: VSPagination;\n isLoading: boolean;\n error: Error | null;\n invalidate: () => void;\n}\n\nexport interface VSUseGetReturn<TDetail> {\n item: TDetail | undefined;\n isLoading: boolean;\n error: Error | null;\n refetch: () => void;\n}\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface VSResourceHooksOptions<TListRaw, TBase, TSingleRaw, TDetail> {\n /**\n * Adapters map your API's raw response to the library's internal format.\n * Omit if your API matches the default shape:\n * List: { data: { items, page, limit, totalPages, totalDocuments } }\n * Single: { data: <item> }\n *\n * @example\n * createResourceHooks(\"contacts\", ContactsApis, {\n * adapters: {\n * fromList: (raw) => ({\n * items: raw.results,\n * pagination: {\n * page: raw.meta.page,\n * limit: raw.meta.perPage,\n * totalPages: raw.meta.lastPage,\n * totalDocuments: raw.meta.total,\n * },\n * }),\n * fromSingle: (raw) => raw.payload,\n * },\n * })\n */\n adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n\n /**\n * Default params passed to useList and useInfinite when none are provided.\n * @default { page: 1, limit: 10 }\n */\n defaultParams?: VSQueryParams;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates a set of TanStack Query hooks for a resource.\n * All types are fully inferred from the `apiService` instance — no generics needed.\n *\n * @param queryKeyPrefix - TanStack Query cache key. Used to scope the cache\n * and for auto-invalidation. e.g. \"contacts\"\n * @param apiService - An instance of ResourceService (or a subclass).\n * @param options - Optional adapters and default params.\n *\n * @example\n * export const contactHooks = createResourceHooks('contacts', ContactsApis);\n *\n * // useList — generic fixed shape\n * const { list, isLoading, pagination, error, invalidate } = contactHooks.useList();\n * // list is typed as Contact.Base[] ✅\n *\n * // useGet\n * const { item, isLoading, error, refetch } = contactHooks.useGet(id);\n * // item is typed as Contact.WithCreatedBy ✅\n *\n * // useMutations\n * const { create, update, remove } = contactHooks.useMutations();\n */\nexport function createResourceHooks<\n K extends string,\n S extends WithResourceTypes,\n>(\n queryKeyPrefix: K,\n apiService: S,\n options: VSResourceHooksOptions<\n ListRaw<S>,\n Base<S>,\n SingleRaw<S>,\n Detail<S>\n > = {},\n) {\n const service = apiService as unknown as ResourceService<\n Id<S>, Base<S>, Detail<S>, Create<S>, Update<S>, ListRaw<S>, SingleRaw<S>\n >;\n\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as VSAdapters<\n VSDefaultPaginatedResponse<Base<S>>,\n Base<S>,\n VSDefaultSingleResponse<Detail<S>>,\n Detail<S>\n > as VSAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n } = options;\n\n return {\n // -------------------------------------------------------------------------\n // useList — fixed generic shape: { list, isLoading, pagination, error, invalidate }\n // -------------------------------------------------------------------------\n useList: (\n params: VSQueryParams = defaultParams,\n ): VSUseListReturn<Base<S>> => {\n const queryClient = useQueryClient();\n\n const query = useQuery<VSListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, params],\n queryFn: async () => {\n const raw = await service.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n return {\n list: query.data?.items ?? [],\n pagination: query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n },\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n invalidate: () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n };\n },\n\n // -------------------------------------------------------------------------\n // useGet — { item, isLoading, error, refetch }\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000): VSUseGetReturn<Detail<S>> => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await service.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n item: query.data,\n isLoading: query.isLoading || query.isFetching,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations — create / update / remove (not delete — reserved keyword)\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () =>\n queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n const createMutation = useMutation<Detail<S>, Error, Create<S>>({\n mutationFn: async (payload) => {\n const raw = await service.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const updateMutation = useMutation<\n Detail<S>,\n Error,\n { _id: Id<S>; payload: Update<S> }\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await service.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n const removeMutation = useMutation<Detail<S>, Error, Id<S>>({\n mutationFn: async (_id) => {\n const raw = await service.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onSuccess: invalidate,\n });\n\n return {\n create: createMutation,\n update: updateMutation,\n remove: removeMutation,\n };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite — infinite scroll / load more\n // -------------------------------------------------------------------------\n useInfinite: (params: VSQueryParams = defaultParams) => {\n return useInfiniteQuery<VSListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await service.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n","import { type ReactNode, useCallback, useState } from \"react\";\n\nexport type VSAlertVariant = \"success\" | \"info\" | \"error\";\n\nexport interface VSAlertState {\n message: ReactNode | string;\n type: VSAlertVariant;\n isVisible: boolean;\n}\n\n/**\n * Manages alert/toast message state with optional auto-hide.\n *\n * @param autoHideDuration - ms before alert hides automatically. Pass 0 to disable. Default: 3000\n *\n * @example\n * const { alert, showAlert, hideAlert } = useAlertMessage();\n * showAlert('Saved successfully!', 'success');\n * showAlert(<b>Something went wrong</b>, 'error');\n */\nexport function useAlertMessage(autoHideDuration = 3000) {\n const [alert, setAlert] = useState<VSAlertState>({\n message: null,\n type: \"info\",\n isVisible: false,\n });\n\n const showAlert = useCallback(\n (message: ReactNode | string, type: VSAlertVariant = \"info\") => {\n setAlert({ message, type, isVisible: true });\n if (autoHideDuration) {\n setTimeout(() => {\n setAlert((prev) => ({ ...prev, isVisible: false }));\n }, autoHideDuration);\n }\n },\n [autoHideDuration]\n );\n\n const hideAlert = useCallback(() => {\n setAlert((prev) => ({ ...prev, isVisible: false }));\n }, []);\n\n return { alert, showAlert, hideAlert };\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { catchError } from \"@void-snippets/core\";\n\nexport type VSAsyncStatus = \"idle\" | \"pending\" | \"success\" | \"error\";\n\ninterface VSAsyncState<T> {\n data: T | null;\n status: VSAsyncStatus;\n error: Error | null;\n}\n\nexport interface VSUseAsyncStateReturn<T> extends VSAsyncState<T> {\n isLoading: boolean;\n isSuccess: boolean;\n isError: boolean;\n\n setData: (data: T | null) => void;\n setError: (error: Error | null) => void;\n reset: () => void;\n\n /**\n * Executes an async function, updates state, and returns a [err, data] tuple.\n * Allows immediate result handling without try/catch.\n *\n * @example\n * const [err, data] = await execute(() => ContactsApis.create(payload));\n * if (err) return showAlert(err.message, 'error');\n * showAlert('Created!', 'success');\n */\n execute: (\n asyncFn: () => Promise<T>,\n options?: {\n onSuccess?: (data: T) => void;\n onError?: (error: Error) => void;\n }\n ) => Promise<[Error, null] | [null, T]>;\n}\n\n/**\n * Generic async state machine — tracks data, status, and error for any async operation.\n * Pair with any async function: API calls, file reads, timers, etc.\n *\n * @param initialData - Optional initial data value. Default: null\n *\n * @example\n * const { data, isLoading, isError, execute } = useAsyncState<User>();\n *\n * const handleSubmit = async () => {\n * const [err, user] = await execute(() => fetchUser(id));\n * if (err) return;\n * console.log(user.name);\n * };\n */\nexport function useAsyncState<T>(\n initialData: T | null = null\n): VSUseAsyncStateReturn<T> {\n const [state, setState] = useState<VSAsyncState<T>>({\n data: initialData,\n status: \"idle\",\n error: null,\n });\n\n const setData = useCallback((data: T | null) => {\n setState({ data, status: \"success\", error: null });\n }, []);\n\n const setError = useCallback((error: Error | null) => {\n setState((prev) => ({ ...prev, error, status: \"error\" }));\n }, []);\n\n const reset = useCallback(() => {\n setState({ data: initialData, status: \"idle\", error: null });\n }, [initialData]);\n\n const execute = useCallback(\n async (\n asyncFn: () => Promise<T>,\n options?: {\n onSuccess?: (data: T) => void;\n onError?: (error: Error) => void;\n }\n ): Promise<[Error, null] | [null, T]> => {\n setState((prev) => ({ ...prev, status: \"pending\", error: null }));\n\n const [err, res] = await catchError(asyncFn());\n\n if (err) {\n setState((prev) => ({ ...prev, status: \"error\", error: err }));\n options?.onError?.(err);\n return [err, null];\n }\n\n setState({ data: res as T, status: \"success\", error: null });\n options?.onSuccess?.(res as T);\n return [null, res as T];\n },\n []\n );\n\n const flags = useMemo(\n () => ({\n isLoading: state.status === \"pending\",\n isSuccess: state.status === \"success\",\n isError: state.status === \"error\",\n }),\n [state.status]\n );\n\n return { ...state, ...flags, setData, setError, reset, execute };\n}\n","import { useEffect, useState } from \"react\";\n\n/**\n * Tracks elapsed time from a given start timestamp — useful for call durations,\n * countdowns, or any elapsed-time display.\n *\n * @param startedAt - Unix timestamp in ms (e.g. Date.now()). Pass null/undefined to reset.\n * @returns Formatted duration string \"MM:SS\"\n *\n * @example\n * const duration = useCallTimer(call.startedAt);\n * // duration → \"02:45\"\n *\n * // Reset when no active call\n * const duration = useCallTimer(activeCall ? activeCall.startedAt : null);\n */\nexport function useCallTimer(startedAt?: number | null): string {\n const [duration, setDuration] = useState(\"00:00\");\n\n useEffect(() => {\n if (!startedAt) {\n setDuration(\"00:00\");\n return;\n }\n\n const interval = setInterval(() => {\n const diffInSeconds = Math.floor((Date.now() - startedAt) / 1000);\n const minutes = Math.floor(diffInSeconds / 60).toString().padStart(2, \"0\");\n const seconds = (diffInSeconds % 60).toString().padStart(2, \"0\");\n setDuration(`${minutes}:${seconds}`);\n }, 1000);\n\n return () => clearInterval(interval);\n }, [startedAt]);\n\n return duration;\n}\n","import { useCallback, useState } from \"react\";\n\nexport interface VSModalReturn<T> {\n isOpen: boolean;\n data: T | null;\n isLoading: boolean;\n openCreateModal: () => void;\n openEditModal: (editData: T) => void;\n setLoading: (loading: boolean) => void;\n closeModal: () => void;\n setModal: (open: boolean, editData?: T | null) => void;\n}\n\n/**\n * Manages modal open/close state with optional data payload and loading state.\n * Works for both create and edit modals — pass data to distinguish the mode.\n *\n * @typeParam T - The type of data the modal operates on (e.g. a Contact, User, etc.)\n *\n * @example\n * const modal = useModal<Contact.Base>();\n *\n * modal.openCreateModal(); // data → null (create mode)\n * modal.openEditModal(contact); // data → contact (edit mode)\n *\n * if (modal.data) {\n * // Edit mode — modal.data is Contact.Base\n * } else {\n * // Create mode\n * }\n */\nexport function useModal<T = unknown>(): VSModalReturn<T> {\n const [isOpen, setIsOpen] = useState(false);\n const [data, setData] = useState<T | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n const openCreateModal = useCallback(() => {\n setIsOpen(true);\n setData(null);\n }, []);\n\n const openEditModal = useCallback((editData: T) => {\n setIsOpen(true);\n setData(editData);\n }, []);\n\n const setLoading = useCallback((loading: boolean) => {\n setIsLoading(loading);\n }, []);\n\n const closeModal = useCallback(() => {\n setIsOpen(false);\n setData(null);\n }, []);\n\n const setModal = useCallback((open: boolean, editData?: T | null) => {\n setIsOpen(open);\n setData(editData ?? null);\n }, []);\n\n return {\n isOpen,\n data,\n isLoading,\n openCreateModal,\n openEditModal,\n setLoading,\n closeModal,\n setModal,\n };\n}\n","import { useCallback, useState } from \"react\";\nimport type { VSQueryParams } from \"@void-snippets/core\";\n\nexport interface VSPaginationReturn {\n page: number;\n limit: number;\n onPaginationChange: (newPage: number, newLimit: number) => void;\n resetPagination: () => void;\n setPage: (page: number) => void;\n setLimit: (limit: number) => void;\n /** Ready-to-use query params object — pass directly to useList() */\n queryParams: VSQueryParams;\n}\n\n/**\n * Manages pagination state and produces a ready-to-use queryParams object\n * compatible with createResourceHooks' useList() and useInfinite().\n *\n * @param initialPage - Starting page. Default: 1\n * @param initialLimit - Items per page. Default: 10\n *\n * @example\n * const { queryParams, onPaginationChange } = usePagination(1, 20);\n *\n * const { list, isLoading } = contactHooks.useList(queryParams);\n *\n * <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />\n */\nexport function usePagination(\n initialPage = 1,\n initialLimit = 10\n): VSPaginationReturn {\n const [page, setPage] = useState(initialPage);\n const [limit, setLimit] = useState(initialLimit);\n\n const onPaginationChange = useCallback(\n (newPage: number, newLimit: number) => {\n setPage(newPage);\n setLimit(newLimit);\n },\n []\n );\n\n const resetPagination = useCallback(() => {\n setPage(1);\n }, []);\n\n return {\n page,\n limit,\n onPaginationChange,\n resetPagination,\n setPage,\n setLimit,\n queryParams: { page, limit },\n };\n}\n"],"mappings":"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","names":["useCallback","useState","useState","useCallback","useState","useCallback","useState"]}
1
+ {"version":3,"sources":["../src/hooks/createResourceHooks.ts","../src/socket/createSocketHooks.ts","../src/routing/createRouteContract.ts","../src/hooks/useAlertMessage.ts","../src/hooks/useAsyncState.ts","../src/hooks/useCallTimer.ts","../src/hooks/useModal.ts","../src/hooks/usePagination.ts"],"sourcesContent":["import {\n useInfiniteQuery,\n useMutation,\n useQuery,\n useQueryClient,\n type InfiniteData,\n type QueryClient,\n type QueryKey,\n} from \"@tanstack/react-query\";\nimport type { ResourceService } from \"@void-snippets/client\";\nimport type {\n VSAdapters,\n VSListResult,\n VSPagination,\n VSQueryParams,\n VSDefaultPaginatedResponse,\n VSDefaultSingleResponse,\n} from \"@void-snippets/core\";\nimport { createDefaultAdapters } from \"@void-snippets/core\";\n\n// ============================================================================\n// TYPE HELPERS\n// ============================================================================\n\nconst DEFAULT_PAGINATION: VSQueryParams = { page: 1, limit: 10 };\n\ninterface WithResourceTypes {\n readonly __types: {\n id: unknown;\n base: unknown;\n detail: unknown;\n create: unknown;\n update: unknown;\n listRaw: unknown;\n singleRaw: unknown;\n };\n}\n\ntype Id<S extends WithResourceTypes> = S[\"__types\"][\"id\"];\ntype Base<S extends WithResourceTypes> = S[\"__types\"][\"base\"];\ntype Detail<S extends WithResourceTypes> = S[\"__types\"][\"detail\"];\ntype Create<S extends WithResourceTypes> = S[\"__types\"][\"create\"];\ntype Update<S extends WithResourceTypes> = S[\"__types\"][\"update\"];\ntype ListRaw<S extends WithResourceTypes> = S[\"__types\"][\"listRaw\"];\ntype SingleRaw<S extends WithResourceTypes> = S[\"__types\"][\"singleRaw\"];\n\n// ============================================================================\n// RETURN TYPES\n// ============================================================================\n\nexport interface VSUseListReturn<TBase> {\n list: TBase[];\n pagination: VSPagination;\n\n /**\n * True only on the very first fetch when there is no cached data yet.\n * Use this to render a full-page spinner or skeleton.\n * Does NOT become true during background refetches — for that see `isRefetching`.\n */\n isLoading: boolean;\n\n /**\n * True during any fetch in progress — initial load or background refetch.\n * Use for a subtle progress indicator that doesn't blank out the list.\n */\n isFetching: boolean;\n\n /**\n * True during a background refetch while cached data is already present.\n * Equivalent to `isFetching && !isLoading`.\n * Use for a lightweight \"Refreshing…\" badge overlaid on the existing list.\n */\n isRefetching: boolean;\n\n /** True when the last fetch attempt resulted in an error. */\n isError: boolean;\n error: Error | null;\n\n /**\n * Re-runs this specific query. Wire to a retry button in your error state.\n * Narrower than `invalidate` — only refetches this exact param variant.\n */\n refetch: () => Promise<unknown>;\n\n /**\n * Marks all active queries under this resource prefix as stale and\n * triggers a background refetch. Broader than `refetch` — affects every\n * mounted param variant of this resource simultaneously.\n */\n invalidate: () => void;\n}\n\nexport interface VSUseGetReturn<TDetail> {\n item: TDetail | undefined;\n\n /** True only on the first fetch when no cached data exists. */\n isLoading: boolean;\n\n /** True during any fetch in progress. */\n isFetching: boolean;\n\n /** True during a background refetch while cached data is already present. */\n isRefetching: boolean;\n\n /** True when the last fetch attempt resulted in an error. */\n isError: boolean;\n error: Error | null;\n\n /** Re-runs this query. */\n refetch: () => Promise<unknown>;\n}\n\n// ============================================================================\n// OPTIMISTIC TYPES\n// ============================================================================\n\n/**\n * The operation context passed to `onError` and `onSuccess` callbacks.\n * Discriminate by `kind` to handle each mutation type differently.\n */\nexport type VSOptimisticOperation<TId, TCreate, TUpdate> =\n | { kind: \"create\"; payload: TCreate; tempId: string }\n | { kind: \"update\"; _id: TId; payload: TUpdate }\n | { kind: \"remove\"; _id: TId };\n\nexport interface VSOptimisticHandlers<\n TBase,\n TId,\n TUpdate,\n TCreate = Partial<TBase>,\n TDetail = TBase\n> {\n /**\n * Optimistically transforms the list when `update.mutate()` fires.\n * `_id` is the mutation target — it is **separate** from `payload`.\n * Applied to every active `useList` and `useInfinite` cache.\n * The `useGet` cache is shallow-merged automatically; override with `updateSingle`.\n * Return a new array — never mutate `cache` in place.\n *\n * @example\n * update: (cache, { _id, payload }) =>\n * cache.map(item => item._id === _id ? { ...item, ...payload } : item)\n */\n update?: (cache: TBase[], args: { _id: TId; payload: TUpdate }) => TBase[];\n\n /**\n * Overrides the default `{ ...current, ...payload }` shallow-merge for the\n * `useGet` cache. Only needed when `TDetail` has nested objects requiring\n * a deep merge. Ignored if `update` is not also provided.\n */\n updateSingle?: (current: TDetail, payload: TUpdate) => TDetail;\n\n /**\n * Optimistically removes an item when `remove.mutate()` fires.\n * `totalDocuments` / `totalPages` are patched automatically from the diff.\n * The matching `useGet` entry is staled (keeps showing data until confirmed).\n * Return a new array — never mutate `cache` in place.\n *\n * @example\n * remove: (cache, id) => cache.filter(item => item._id !== id)\n */\n remove?: (cache: TBase[], id: TId) => TBase[];\n\n /**\n * Optimistically inserts an item when `create.mutate()` fires.\n * Applied to every `useList` cache and the **first page** of every\n * `useInfinite` cache. `totalDocuments` / `totalPages` are patched automatically.\n * `tempId` is a library-generated UUID — use it to set `_id` on the item.\n * Return a new array — never mutate `cache` in place.\n *\n * @example\n * create: (cache, { payload, tempId }) => [\n * { ...payload, _id: tempId as Contact.Id },\n * ...cache,\n * ]\n */\n create?: (cache: TBase[], args: { payload: TCreate; tempId: string }) => TBase[];\n\n /**\n * Called after the optimistic rollback completes when a mutation fails.\n * The cache is already restored to the correct state when this fires.\n * Use for resource-level error notifications across all call sites.\n *\n * @example\n * onError: (error, operation) => {\n * toast.error(`Failed to ${operation.kind} item: ${error.message}`)\n * }\n */\n onError?: (\n error: Error,\n operation: VSOptimisticOperation<TId, TCreate, TUpdate>,\n ) => void;\n\n /**\n * Called after `effectiveBase` has been advanced for this operation.\n * Fires once per successfully confirmed mutation, before the final\n * invalidation when all pending operations have settled.\n *\n * @example\n * onSuccess: (operation) => {\n * if (operation.kind === 'create') analytics.track('item_created')\n * }\n */\n onSuccess?: (operation: VSOptimisticOperation<TId, TCreate, TUpdate>) => void;\n}\n\n// ============================================================================\n// OPTIONS\n// ============================================================================\n\nexport interface VSResourceHooksOptions<\n TListRaw,\n TBase,\n TSingleRaw,\n TDetail,\n TId = unknown,\n TUpdate = unknown,\n TCreate = unknown\n> {\n adapters?: VSAdapters<TListRaw, TBase, TSingleRaw, TDetail>;\n defaultParams?: VSQueryParams;\n optimistic?: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>;\n}\n\n// ============================================================================\n// INTERNAL: OPTIMISTIC STACK\n// ============================================================================\n\ntype PendingOp<TId, TUpdate, TCreate> =\n | { id: symbol; kind: \"update\"; _id: TId; payload: TUpdate }\n | { id: symbol; kind: \"remove\"; _id: TId }\n | { id: symbol; kind: \"create\"; payload: TCreate; tempId: string };\n\ninterface OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail> {\n pendingOps: PendingOp<TId, TUpdate, TCreate>[];\n effectiveBaseListSnapshots: [QueryKey, VSListResult<TBase> | undefined][];\n effectiveBaseInfiniteSnapshots: [QueryKey, InfiniteData<VSListResult<TBase>> | undefined][];\n effectiveBaseGet: Map<string, TDetail | undefined>;\n}\n\ntype MutationContext = { operationId: symbol };\n\n/** Strips the internal `id: symbol` field to produce the public operation shape. */\nfunction toOperation<TId, TUpdate, TCreate>(\n op: PendingOp<TId, TUpdate, TCreate>,\n): VSOptimisticOperation<TId, TCreate, TUpdate> {\n if (op.kind === \"create\") return { kind: \"create\", payload: op.payload, tempId: op.tempId };\n if (op.kind === \"update\") return { kind: \"update\", _id: op._id, payload: op.payload };\n return { kind: \"remove\", _id: op._id };\n}\n\n// ============================================================================\n// INTERNAL: STACK STORAGE\n// WeakMap<QueryClient> → no leaks on client destruction, SSR-safe isolation\n// ============================================================================\n\nconst _stacks = new WeakMap<\n object,\n Map<string, OptimisticStack<any, any, any, any, any>>\n>();\n\nfunction getStack<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n): OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail> {\n if (!_stacks.has(client)) _stacks.set(client, new Map());\n const map = _stacks.get(client)!;\n if (!map.has(prefix)) {\n map.set(prefix, {\n pendingOps: [],\n effectiveBaseListSnapshots: [],\n effectiveBaseInfiniteSnapshots: [],\n effectiveBaseGet: new Map(),\n });\n }\n return map.get(prefix)! as OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail>;\n}\n\n// ============================================================================\n// INTERNAL: QUERY KEY DISCRIMINATOR\n// useList → [prefix, VSQueryParams] key[1] is a plain object\n// useGet → [prefix, TId] key[1] is a string\n// useInfinite→ [prefix, \"INFINITE\", …] length > 2\n// ============================================================================\n\nfunction isListQueryKey(query: { queryKey: readonly unknown[] }): boolean {\n const key = query.queryKey;\n return (\n key.length === 2 &&\n key[1] !== null &&\n typeof key[1] === \"object\" &&\n !Array.isArray(key[1])\n );\n}\n\n// ============================================================================\n// INTERNAL: TEMP ID GENERATOR\n// ============================================================================\n\nfunction generateTempId(): string {\n if (typeof crypto !== \"undefined\" && typeof crypto.randomUUID === \"function\") {\n return crypto.randomUUID();\n }\n return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n}\n\n// ============================================================================\n// INTERNAL: PAGINATION PATCH\n// ============================================================================\n\nfunction patchPagination(pagination: VSPagination, delta: number): VSPagination {\n if (delta === 0) return pagination;\n const newTotal = Math.max(0, pagination.totalDocuments + delta);\n return {\n ...pagination,\n totalDocuments: newTotal,\n totalPages: Math.ceil(newTotal / pagination.limit),\n };\n}\n\n// ============================================================================\n// INTERNAL: CACHE APPLICATION HELPERS\n// ============================================================================\n\nfunction applyUpdateToAllCaches<TBase, TId, TUpdate>(\n client: QueryClient,\n prefix: string,\n _id: TId,\n payload: TUpdate,\n handler: (cache: TBase[], args: { _id: TId; payload: TUpdate }) => TBase[],\n): void {\n client.setQueriesData<VSListResult<TBase>>(\n { queryKey: [prefix], predicate: isListQueryKey as any },\n (old) => old ? { ...old, items: handler(old.items, { _id, payload }) } : old,\n );\n client.setQueriesData<InfiniteData<VSListResult<TBase>>>(\n { queryKey: [prefix, \"INFINITE\"] },\n (old) =>\n old\n ? { ...old, pages: old.pages.map((p) => ({ ...p, items: handler(p.items, { _id, payload }) })) }\n : old,\n );\n}\n\nfunction applyRemoveFromAllCaches<TBase, TId>(\n client: QueryClient,\n prefix: string,\n _id: TId,\n handler: (cache: TBase[], id: TId) => TBase[],\n): void {\n client.setQueriesData<VSListResult<TBase>>(\n { queryKey: [prefix], predicate: isListQueryKey as any },\n (old) => {\n if (!old) return old;\n const newItems = handler(old.items, _id);\n return { ...old, items: newItems, pagination: patchPagination(old.pagination, newItems.length - old.items.length) };\n },\n );\n client.setQueriesData<InfiniteData<VSListResult<TBase>>>(\n { queryKey: [prefix, \"INFINITE\"] },\n (old) =>\n old\n ? {\n ...old,\n pages: old.pages.map((p) => {\n const newItems = handler(p.items, _id);\n return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };\n }),\n }\n : old,\n );\n}\n\nfunction applyCreateToAllCaches<TBase, TCreate>(\n client: QueryClient,\n prefix: string,\n payload: TCreate,\n tempId: string,\n handler: (cache: TBase[], args: { payload: TCreate; tempId: string }) => TBase[],\n): void {\n client.setQueriesData<VSListResult<TBase>>(\n { queryKey: [prefix], predicate: isListQueryKey as any },\n (old) => {\n if (!old) return old;\n const newItems = handler(old.items, { payload, tempId });\n return { ...old, items: newItems, pagination: patchPagination(old.pagination, newItems.length - old.items.length) };\n },\n );\n // First page only — new items belong at the top of the feed, not duplicated\n client.setQueriesData<InfiniteData<VSListResult<TBase>>>(\n { queryKey: [prefix, \"INFINITE\"] },\n (old) => {\n if (!old) return old;\n return {\n ...old,\n pages: old.pages.map((p, i) => {\n if (i !== 0) return p;\n const newItems = handler(p.items, { payload, tempId });\n return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };\n }),\n };\n },\n );\n}\n\n// ============================================================================\n// INTERNAL: EFFECTIVE BASE OPERATIONS\n// ============================================================================\n\nfunction restoreEffectiveBase<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n stack: OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n stack.effectiveBaseListSnapshots.forEach(([key, data]) => client.setQueryData(key, data));\n stack.effectiveBaseInfiniteSnapshots.forEach(([key, data]) => client.setQueryData(key, data));\n stack.effectiveBaseGet.forEach((data, idStr) => client.setQueryData([prefix, idStr], data));\n}\n\nfunction advanceEffectiveBase<TBase, TId, TUpdate, TCreate, TDetail>(\n stack: OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail>,\n op: PendingOp<TId, TUpdate, TCreate>,\n optimistic: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n if (op.kind === \"update\" && optimistic.update) {\n stack.effectiveBaseListSnapshots = stack.effectiveBaseListSnapshots.map(([key, data]) =>\n data ? [key, { ...data, items: optimistic.update!(data.items, { _id: op._id, payload: op.payload }) }] : [key, data],\n );\n stack.effectiveBaseInfiniteSnapshots = stack.effectiveBaseInfiniteSnapshots.map(([key, data]) =>\n data\n ? [key, { ...data, pages: data.pages.map((p) => ({ ...p, items: optimistic.update!(p.items, { _id: op._id, payload: op.payload }) })) }]\n : [key, data],\n );\n const baseEntry = stack.effectiveBaseGet.get(String(op._id));\n if (baseEntry !== undefined) {\n const advanced = optimistic.updateSingle\n ? optimistic.updateSingle(baseEntry, op.payload)\n : ({ ...baseEntry, ...(op.payload as object) } as TDetail);\n stack.effectiveBaseGet.set(String(op._id), advanced);\n }\n return;\n }\n\n if (op.kind === \"remove\" && optimistic.remove) {\n stack.effectiveBaseListSnapshots = stack.effectiveBaseListSnapshots.map(([key, data]) => {\n if (!data) return [key, data];\n const newItems = optimistic.remove!(data.items, op._id);\n return [key, { ...data, items: newItems, pagination: patchPagination(data.pagination, newItems.length - data.items.length) }];\n });\n stack.effectiveBaseInfiniteSnapshots = stack.effectiveBaseInfiniteSnapshots.map(([key, data]) =>\n data\n ? [\n key,\n {\n ...data,\n pages: data.pages.map((p) => {\n const newItems = optimistic.remove!(p.items, op._id);\n return { ...p, items: newItems, pagination: patchPagination(p.pagination, newItems.length - p.items.length) };\n }),\n },\n ]\n : [key, data],\n );\n stack.effectiveBaseGet.delete(String(op._id));\n return;\n }\n // create: effectiveBase intentionally not advanced.\n // The temp item is replaced by server data when all ops settle and invalidation fires.\n}\n\nfunction replayPendingOps<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n stack: OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail>,\n optimistic: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n for (const op of stack.pendingOps) {\n if (op.kind === \"update\" && optimistic.update) {\n applyUpdateToAllCaches(client, prefix, op._id, op.payload, optimistic.update);\n const current = client.getQueryData<TDetail>([prefix, String(op._id)]);\n if (current !== undefined) {\n const updated = optimistic.updateSingle\n ? optimistic.updateSingle(current, op.payload)\n : ({ ...current, ...(op.payload as object) } as TDetail);\n client.setQueryData([prefix, String(op._id)], updated);\n }\n } else if (op.kind === \"remove\" && optimistic.remove) {\n applyRemoveFromAllCaches(client, prefix, op._id, optimistic.remove);\n client.invalidateQueries({ queryKey: [prefix, String(op._id)], refetchType: \"none\" });\n } else if (op.kind === \"create\" && optimistic.create) {\n applyCreateToAllCaches(client, prefix, op.payload, op.tempId, optimistic.create);\n }\n }\n}\n\nfunction flushStack<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n stack: OptimisticStack<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n stack.pendingOps = [];\n stack.effectiveBaseListSnapshots = [];\n stack.effectiveBaseInfiniteSnapshots = [];\n stack.effectiveBaseGet.clear();\n client.invalidateQueries({ queryKey: [prefix] });\n}\n\n// ============================================================================\n// INTERNAL: SHARED MUTATION LIFECYCLE HANDLERS\n// ============================================================================\n\nfunction handleOptimisticError<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n error: Error,\n context: MutationContext | undefined,\n optimistic: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n if (!context) return;\n\n const stack = getStack<TBase, TId, TUpdate, TCreate, TDetail>(client, prefix);\n\n // Find the failed op before removing it — needed for the onError callback\n const failedOp = stack.pendingOps.find((op) => op.id === context.operationId);\n\n stack.pendingOps = stack.pendingOps.filter((op) => op.id !== context.operationId);\n restoreEffectiveBase(client, prefix, stack);\n replayPendingOps(client, prefix, stack, optimistic);\n\n // Fire AFTER rollback — cache is consistent when developer code runs.\n // Wrapped in try/catch so a throwing user callback cannot corrupt internal state.\n if (failedOp && optimistic.onError) {\n try {\n optimistic.onError(error, toOperation(failedOp));\n } catch {\n // Swallow — developer callbacks must not interrupt library lifecycle\n }\n }\n}\n\nfunction handleOptimisticSettled<TBase, TId, TUpdate, TCreate, TDetail>(\n client: QueryClient,\n prefix: string,\n error: Error | null,\n context: MutationContext | undefined,\n optimistic: VSOptimisticHandlers<TBase, TId, TUpdate, TCreate, TDetail>,\n): void {\n // onMutate threw before returning — fall back to plain invalidation\n if (!context) {\n client.invalidateQueries({ queryKey: [prefix] });\n return;\n }\n\n const stack = getStack<TBase, TId, TUpdate, TCreate, TDetail>(client, prefix);\n\n if (!error) {\n // Success path: advance the confirmed floor before removing the op\n const settledOp = stack.pendingOps.find((op) => op.id === context.operationId);\n if (settledOp) {\n advanceEffectiveBase(stack, settledOp, optimistic);\n\n // Fire onSuccess after advance — developer sees the confirmed cache state.\n if (optimistic.onSuccess) {\n try {\n optimistic.onSuccess(toOperation(settledOp));\n } catch {\n // Swallow — developer callbacks must not interrupt library lifecycle\n }\n }\n }\n }\n\n // Remove this op — idempotent on the error path since onError already removed it\n stack.pendingOps = stack.pendingOps.filter((op) => op.id !== context.operationId);\n\n if (stack.pendingOps.length === 0) {\n // All mutations settled — sync with server now\n flushStack(client, prefix, stack);\n }\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\nexport function createResourceHooks<K extends string, S extends WithResourceTypes>(\n queryKeyPrefix: K,\n apiService: S,\n options: VSResourceHooksOptions<\n ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>, Id<S>, Update<S>, Create<S>\n > = {},\n) {\n const service = apiService as unknown as ResourceService<\n Id<S>, Base<S>, Detail<S>, Create<S>, Update<S>, ListRaw<S>, SingleRaw<S>\n >;\n\n const {\n adapters = createDefaultAdapters<Base<S>, Detail<S>>() as VSAdapters<\n VSDefaultPaginatedResponse<Base<S>>, Base<S>, VSDefaultSingleResponse<Detail<S>>, Detail<S>\n > as VSAdapters<ListRaw<S>, Base<S>, SingleRaw<S>, Detail<S>>,\n defaultParams = DEFAULT_PAGINATION,\n optimistic,\n } = options;\n\n return {\n // -------------------------------------------------------------------------\n // useList\n // -------------------------------------------------------------------------\n useList: (params: VSQueryParams = defaultParams): VSUseListReturn<Base<S>> => {\n const queryClient = useQueryClient();\n\n const query = useQuery<VSListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, params],\n queryFn: async () => {\n const raw = await service.list(params);\n return adapters.fromList(raw as ListRaw<S>);\n },\n });\n\n return {\n list: query.data?.items ?? [],\n pagination: query.data?.pagination ?? {\n page: 1,\n limit: defaultParams.limit ?? 10,\n totalPages: 0,\n totalDocuments: 0,\n },\n isLoading: query.isLoading,\n isFetching: query.isFetching,\n isRefetching: query.isRefetching,\n isError: query.isError,\n error: query.error,\n refetch: query.refetch,\n invalidate: () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] }),\n };\n },\n\n // -------------------------------------------------------------------------\n // useGet\n // -------------------------------------------------------------------------\n useGet: (id: Id<S>, staleTime = 30_000): VSUseGetReturn<Detail<S>> => {\n const query = useQuery<Detail<S>, Error>({\n queryKey: [queryKeyPrefix, id],\n queryFn: async () => {\n const raw = await service.get(id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n enabled: id !== undefined && id !== null && id !== \"\",\n staleTime,\n });\n\n return {\n item: query.data,\n isLoading: query.isLoading,\n isFetching: query.isFetching,\n isRefetching: query.isRefetching,\n isError: query.isError,\n error: query.error,\n refetch: query.refetch,\n };\n },\n\n // -------------------------------------------------------------------------\n // useMutations\n // -------------------------------------------------------------------------\n useMutations: () => {\n const queryClient = useQueryClient();\n const invalidate = () => queryClient.invalidateQueries({ queryKey: [queryKeyPrefix] });\n\n // Synchronously captures base snapshots the moment the first operation\n // fires. Called AFTER cancelQueries so the snapshot is always stable.\n function captureBaseIfFirstOp(\n stack: OptimisticStack<Base<S>, Id<S>, Update<S>, Create<S>, Detail<S>>,\n ): void {\n if (stack.pendingOps.length === 0) {\n stack.effectiveBaseListSnapshots = queryClient.getQueriesData<VSListResult<Base<S>>>({\n queryKey: [queryKeyPrefix],\n predicate: isListQueryKey as any,\n });\n stack.effectiveBaseInfiniteSnapshots = queryClient.getQueriesData<InfiniteData<VSListResult<Base<S>>>>({\n queryKey: [queryKeyPrefix, \"INFINITE\"],\n });\n }\n }\n\n // -----------------------------------------------------------------------\n // createMutation\n // -----------------------------------------------------------------------\n const createMutation = useMutation<Detail<S>, Error, Create<S>, MutationContext | undefined>({\n mutationFn: async (payload) => {\n const raw = await service.create(payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onMutate: async (payload) => {\n if (!optimistic?.create) return undefined;\n\n // 1. Cancel first — prevents in-flight refetches from overwriting\n // the optimistic write after it lands.\n await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });\n\n const stack = getStack<Base<S>, Id<S>, Update<S>, Create<S>, Detail<S>>(\n queryClient, queryKeyPrefix,\n );\n\n // 2. Snapshot the now-stable cache (no fetches in flight).\n captureBaseIfFirstOp(stack);\n\n // 3. Register the operation.\n const tempId = generateTempId();\n const op: PendingOp<Id<S>, Update<S>, Create<S>> = {\n id: Symbol(), kind: \"create\", payload, tempId,\n };\n stack.pendingOps.push(op);\n\n // 4. Apply — fully synchronous from here, no race window.\n try {\n applyCreateToAllCaches(queryClient, queryKeyPrefix, payload, tempId, optimistic.create);\n } catch {\n stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);\n throw new Error(\"[@void-snippets/react] Optimistic create setup failed.\");\n }\n\n return { operationId: op.id };\n },\n onError: (_err, _vars, context) => {\n if (!optimistic?.create) return;\n handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);\n },\n onSettled: (_data, error, _vars, context) => {\n if (!optimistic?.create) { invalidate(); return; }\n handleOptimisticSettled(queryClient, queryKeyPrefix, error ?? null, context, optimistic);\n },\n });\n\n // -----------------------------------------------------------------------\n // updateMutation\n // -----------------------------------------------------------------------\n const updateMutation = useMutation<\n Detail<S>, Error, { _id: Id<S>; payload: Update<S> }, MutationContext | undefined\n >({\n mutationFn: async ({ _id, payload }) => {\n const raw = await service.update(_id, payload);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onMutate: async ({ _id, payload }) => {\n if (!optimistic?.update) return undefined;\n\n await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });\n\n const stack = getStack<Base<S>, Id<S>, Update<S>, Create<S>, Detail<S>>(\n queryClient, queryKeyPrefix,\n );\n\n captureBaseIfFirstOp(stack);\n\n const currentSingle = queryClient.getQueryData<Detail<S>>([queryKeyPrefix, String(_id)]);\n if (currentSingle !== undefined) {\n stack.effectiveBaseGet.set(String(_id), currentSingle);\n }\n\n const op: PendingOp<Id<S>, Update<S>, Create<S>> = {\n id: Symbol(), kind: \"update\", _id, payload,\n };\n stack.pendingOps.push(op);\n\n try {\n applyUpdateToAllCaches(queryClient, queryKeyPrefix, _id, payload, optimistic.update);\n\n if (currentSingle !== undefined) {\n const updated = optimistic.updateSingle\n ? optimistic.updateSingle(currentSingle, payload)\n : ({ ...currentSingle, ...(payload as object) } as Detail<S>);\n queryClient.setQueryData([queryKeyPrefix, String(_id)], updated);\n }\n } catch {\n stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);\n throw new Error(\"[@void-snippets/react] Optimistic update setup failed.\");\n }\n\n return { operationId: op.id };\n },\n onError: (_err, _vars, context) => {\n if (!optimistic?.update) return;\n handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);\n },\n onSettled: (_data, error, _vars, context) => {\n if (!optimistic?.update) { invalidate(); return; }\n handleOptimisticSettled(queryClient, queryKeyPrefix, error ?? null, context, optimistic);\n },\n });\n\n // -----------------------------------------------------------------------\n // removeMutation\n // -----------------------------------------------------------------------\n const removeMutation = useMutation<Detail<S>, Error, Id<S>, MutationContext | undefined>({\n mutationFn: async (_id) => {\n const raw = await service.delete(_id);\n return adapters.fromSingle(raw as SingleRaw<S>);\n },\n onMutate: async (_id) => {\n if (!optimistic?.remove) return undefined;\n\n await queryClient.cancelQueries({ queryKey: [queryKeyPrefix] });\n\n const stack = getStack<Base<S>, Id<S>, Update<S>, Create<S>, Detail<S>>(\n queryClient, queryKeyPrefix,\n );\n\n captureBaseIfFirstOp(stack);\n\n const currentSingle = queryClient.getQueryData<Detail<S>>([queryKeyPrefix, String(_id)]);\n if (currentSingle !== undefined) {\n stack.effectiveBaseGet.set(String(_id), currentSingle);\n }\n\n const op: PendingOp<Id<S>, Update<S>, Create<S>> = {\n id: Symbol(), kind: \"remove\", _id,\n };\n stack.pendingOps.push(op);\n\n try {\n applyRemoveFromAllCaches(queryClient, queryKeyPrefix, _id, optimistic.remove);\n\n if (currentSingle !== undefined) {\n queryClient.invalidateQueries({\n queryKey: [queryKeyPrefix, String(_id)],\n refetchType: \"none\",\n });\n }\n } catch {\n stack.pendingOps = stack.pendingOps.filter((o) => o.id !== op.id);\n throw new Error(\"[@void-snippets/react] Optimistic remove setup failed.\");\n }\n\n return { operationId: op.id };\n },\n onError: (_err, _vars, context) => {\n if (!optimistic?.remove) return;\n handleOptimisticError(queryClient, queryKeyPrefix, _err, context, optimistic);\n },\n onSettled: (_data, error, _vars, context) => {\n if (!optimistic?.remove) { invalidate(); return; }\n handleOptimisticSettled(queryClient, queryKeyPrefix, error ?? null, context, optimistic);\n },\n });\n\n return { create: createMutation, update: updateMutation, remove: removeMutation };\n },\n\n // -------------------------------------------------------------------------\n // useInfinite\n // -------------------------------------------------------------------------\n useInfinite: (params: VSQueryParams = defaultParams) => {\n return useInfiniteQuery<VSListResult<Base<S>>, Error>({\n queryKey: [queryKeyPrefix, \"INFINITE\", params],\n queryFn: async ({ pageParam }) => {\n const raw = await service.list({\n ...params,\n page: pageParam as number,\n limit: params.limit ?? 20,\n });\n return adapters.fromList(raw as ListRaw<S>);\n },\n getNextPageParam: (lastPage) => {\n const { page, totalPages } = lastPage.pagination;\n return page < totalPages ? page + 1 : undefined;\n },\n initialPageParam: 1,\n });\n },\n };\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { Socket } from \"socket.io-client\";\n\n// ============================================================================\n// INTERNAL TYPE UTILITIES\n// ============================================================================\n\n// All runtime parameters of an event handler\ntype EventParams<TEvents, K extends keyof TEvents> =\n TEvents[K] extends (...args: infer P) => void ? P : never;\n\n// True when the last element of a tuple is callable (an ACK callback)\ntype LastIsCallback<P extends readonly unknown[]> =\n P extends [...infer _Rest, infer Last]\n ? Last extends (...args: any[]) => void\n ? true\n : false\n : false;\n\n// Params with the trailing ACK callback stripped off.\n// For events that have no callback, all params are returned unchanged.\n// join-room(roomId: string) → [string]\n// send-message(msg: { text; roomId }) → [{ text; roomId }]\n// update-profile(name: string, cb: (r) => void) → [string]\ntype NoAckArgs<TEvents, K extends keyof TEvents> =\n EventParams<TEvents, K> extends [...infer Rest, infer Last]\n ? Last extends (...args: any[]) => void\n ? Rest\n : EventParams<TEvents, K>\n : EventParams<TEvents, K>;\n\n// Keys of TClientEvents whose last param is an ACK callback.\n// TypeScript will error at compile time if emitWithAck is called on any other key.\ntype AckEventKeys<TEvents> = {\n [K in keyof TEvents]: LastIsCallback<\n TEvents[K] extends (...args: infer P) => void ? P : never\n > extends true\n ? K\n : never;\n}[keyof TEvents];\n\n// The first argument of the ACK callback — what the server sends back.\n// update-profile(name, (r: { status: \"ok\" | \"error\" }) => void)\n// → AckResponseType = { status: \"ok\" | \"error\" }\ntype AckResponseType<TEvents, K extends keyof TEvents> =\n EventParams<TEvents, K> extends [...infer _Rest, infer Last]\n ? Last extends (arg: infer R, ...rest: any[]) => void\n ? R\n : never\n : never;\n\n// ============================================================================\n// PUBLIC RETURN TYPES\n// ============================================================================\n\nexport interface VSSocketConnectionReturn {\n /** True when the socket has an active, confirmed connection. */\n isConnected: boolean;\n\n /**\n * True while a connection or reconnection attempt is in progress.\n * Resets to false when `connect` or `connect_error` fires.\n */\n isConnecting: boolean;\n\n /** The socket ID assigned by the server. Undefined when disconnected. */\n socketId: string | undefined;\n\n /** The error from the last failed connection attempt. Null on success or before any attempt. */\n error: Error | null;\n\n /**\n * Initiates a connection. No-op if already connected.\n * Sets `isConnecting: true` until `connect` or `connect_error` fires.\n */\n connect: () => void;\n\n /** Gracefully closes the connection and stops all reconnection attempts. */\n disconnect: () => void;\n}\n\n// ============================================================================\n// FACTORY\n// ============================================================================\n\n/**\n * Creates three type-safe Socket.IO hooks bound to a specific socket instance.\n *\n * Call once at module level — the returned hooks close over the socket and both\n * event-map generics, so no type parameters are needed at individual call sites.\n *\n * Requires socket.io-client ≥4.6.0 (for native `socket.emitWithAck` support).\n *\n * @example\n * // socket-hooks.ts (create once, export and use everywhere)\n * import { createSocketHooks } from '@void-snippets/react';\n * import { io } from 'socket.io-client';\n *\n * const socket = io(process.env.SOCKET_URL, { autoConnect: false });\n *\n * export const { useSocketEmit, useSocketListener, useSocketConnection } =\n * createSocketHooks<IClientToServerEvents, IServerToClientEvents>(socket);\n */\nexport function createSocketHooks<\n TClientEvents extends Record<string, (...args: any[]) => void>,\n TServerEvents extends Record<string, (...args: any[]) => void>,\n>(socket: Socket<TServerEvents, TClientEvents>) {\n\n // -------------------------------------------------------------------------\n // useSocketEmit\n // -------------------------------------------------------------------------\n\n /**\n * Returns two functions for type-safe event emission.\n *\n * **`emit(event, ...args)`** — fire and forget, no acknowledgement.\n * Callable on any event regardless of its signature. Throws synchronously\n * if the socket is not connected.\n *\n * **`emitWithAck(event, ...args)`** — emits and returns a `Promise` that\n * resolves with the server's acknowledgement response. TypeScript will error\n * at compile time if called on an event whose type has no callback.\n * Returns a rejected `Promise` if the socket is not connected.\n *\n * @example\n * const { emit, emitWithAck } = useSocketEmit();\n *\n * emit('join-room', roomId);\n *\n * const result = await emitWithAck('update-profile', name);\n * // result is { status: \"ok\" | \"error\" } — inferred from the event type\n */\n function useSocketEmit() {\n const emit = useCallback(\n <K extends keyof TClientEvents>(\n event: K,\n ...args: NoAckArgs<TClientEvents, K>\n ): void => {\n if (!socket.connected) {\n throw new Error(\n `[@void-snippets/react] Cannot emit \"${String(event)}\" — socket is not connected.`,\n );\n }\n // Cast required: TypeScript cannot spread-infer our stripped tuple\n // against socket.emit's overloaded generic signature.\n // The call is equivalent at runtime.\n (socket as any).emit(event, ...(args as unknown[]));\n },\n [],\n );\n\n const emitWithAck = useCallback(\n <K extends AckEventKeys<TClientEvents>>(\n event: K,\n ...args: NoAckArgs<TClientEvents, K>\n ): Promise<AckResponseType<TClientEvents, K>> => {\n if (!socket.connected) {\n return Promise.reject(\n new Error(\n `[@void-snippets/react] Cannot emit \"${String(event)}\" — socket is not connected.`,\n ),\n );\n }\n // socket.emitWithAck is available from socket.io-client ≥4.6.\n return (socket as any).emitWithAck(\n event,\n ...(args as unknown[]),\n ) as Promise<AckResponseType<TClientEvents, K>>;\n },\n [],\n );\n\n return { emit, emitWithAck };\n }\n\n // -------------------------------------------------------------------------\n // useSocketListener\n // -------------------------------------------------------------------------\n\n /**\n * Subscribes to a server event for the lifetime of the calling component.\n *\n * A ref pattern ensures the **latest** version of `handler` is always invoked\n * without re-registering the listener on every render. This means inline arrow\n * functions are safe — no `useCallback` needed at the call site.\n *\n * @param event The server event name to listen for.\n * @param handler Called whenever the event fires. Always reflects the latest reference.\n * @param options\n * `enabled` (default `true`) — when `false`, the listener is not attached.\n * Flip dynamically to activate/deactivate without unmounting the component.\n *\n * @example\n * useSocketListener('new-message', (data) => {\n * setMessages(prev => [...prev, data]);\n * });\n *\n * // Conditional — only listen when a room is selected\n * useSocketListener('user-joined', (userId) => { ... }, { enabled: !!roomId });\n */\n function useSocketListener<K extends keyof TServerEvents>(\n event: K,\n handler: TServerEvents[K],\n options?: { enabled?: boolean },\n ): void {\n const enabled = options?.enabled ?? true;\n\n // Tracks the latest handler without triggering re-registration in the listener effect\n const savedHandler = useRef<TServerEvents[K]>(handler);\n useEffect(() => {\n savedHandler.current = handler;\n }, [handler]);\n\n useEffect(() => {\n if (!enabled) return;\n\n const listener = ((...args: Parameters<TServerEvents[K]>) => {\n (\n savedHandler.current as (\n ...a: Parameters<TServerEvents[K]>\n ) => void\n )(...args);\n }) as TServerEvents[K];\n\n (socket as any).on(event, listener);\n\n return () => {\n (socket as any).off(event, listener);\n };\n\n // `handler` intentionally excluded: the ref update effect handles\n // staleness without re-registering the listener on every render.\n }, [event, enabled]);\n }\n\n // -------------------------------------------------------------------------\n // useSocketConnection\n // -------------------------------------------------------------------------\n\n /**\n * Reactively tracks socket connection state and exposes connect/disconnect controls.\n *\n * Safe to mount from multiple components simultaneously — each instance\n * independently subscribes to the same underlying socket events and reflects\n * the same connection state via local React state.\n *\n * Listens to `connect`, `disconnect`, `connect_error` on the socket, and\n * `reconnect_attempt` / `reconnect_failed` on the Manager (`socket.io`).\n * All listeners are removed on unmount.\n *\n * @example\n * function AppShell() {\n * const { isConnected, isConnecting, error, connect, disconnect } =\n * useSocketConnection();\n *\n * useEffect(() => { connect(); }, []);\n *\n * return (\n * <>\n * {isConnecting && <Banner>Connecting…</Banner>}\n * {error && <Banner type=\"error\">{error.message}</Banner>}\n * <YourApp />\n * </>\n * );\n * }\n */\n function useSocketConnection(): VSSocketConnectionReturn {\n const [isConnected, setIsConnected] = useState<boolean>(socket.connected);\n const [isConnecting, setIsConnecting] = useState<boolean>(false);\n const [socketId, setSocketId] = useState<string | undefined>(socket.id);\n const [error, setError] = useState<Error | null>(null);\n\n useEffect(() => {\n function onConnect() {\n setIsConnected(true);\n setIsConnecting(false);\n setSocketId(socket.id);\n setError(null);\n }\n\n function onDisconnect() {\n setIsConnected(false);\n setIsConnecting(false);\n setSocketId(undefined);\n }\n\n function onConnectError(err: Error) {\n setIsConnected(false);\n setIsConnecting(false);\n setError(err);\n }\n\n // Manager-level events — socket.io is the underlying Manager instance.\n // These fire during reconnection cycles managed by socket.io-client internally.\n function onReconnectAttempt() {\n setIsConnecting(true);\n }\n\n function onReconnectFailed() {\n setIsConnecting(false);\n setError(\n new Error(\n \"[@void-snippets/react] Socket reconnection failed — maximum attempts exceeded.\",\n ),\n );\n }\n\n socket.on(\"connect\", onConnect);\n socket.on(\"disconnect\", onDisconnect);\n socket.on(\"connect_error\", onConnectError);\n\n // Cast: Manager event types vary across socket.io-client minor versions.\n (socket.io as any).on(\"reconnect_attempt\", onReconnectAttempt);\n (socket.io as any).on(\"reconnect_failed\", onReconnectFailed);\n\n return () => {\n socket.off(\"connect\", onConnect);\n socket.off(\"disconnect\", onDisconnect);\n socket.off(\"connect_error\", onConnectError);\n (socket.io as any).off(\"reconnect_attempt\", onReconnectAttempt);\n (socket.io as any).off(\"reconnect_failed\", onReconnectFailed);\n };\n }, []);\n\n const connect = useCallback((): void => {\n if (!socket.connected) {\n setIsConnecting(true);\n socket.connect();\n }\n }, []);\n\n const disconnect = useCallback((): void => {\n socket.disconnect();\n }, []);\n\n return {\n isConnected,\n isConnecting,\n socketId,\n error,\n connect,\n disconnect,\n };\n }\n\n return {\n useSocketEmit,\n useSocketListener,\n useSocketConnection,\n };\n}\n","import { generatePath } from \"react-router\";\nimport { useSearchParams } from \"react-router\";\n\n// ============================================================================\n// INTERNAL TYPE UTILITIES\n// ============================================================================\n\n/**\n * Recursively extracts named path parameters from a route path string using\n * TypeScript template literal inference. Handles both required and optional\n * segments, and composes correctly across nested slashes.\n *\n * '/users/:userId/posts/:postId' → { userId: string | number; postId: string | number }\n * '/files/:path?' → { path?: string | number }\n */\ntype ExtractRouteParams<T extends string> =\n // :param/ — parameter followed by more segments\n T extends `${infer _Start}:${infer Param}/${infer Rest}`\n ? (Param extends `${infer P}?`\n ? { [K in P]?: string | number } // optional param\n : { [K in Param]: string | number }) // required param\n & ExtractRouteParams<`/${Rest}`> // recurse into the remaining path\n // :param — parameter at the end of the path\n : T extends `${infer _Start}:${infer Param}`\n ? Param extends `${infer P}?`\n ? { [K in P]?: string | number } // optional terminal param\n : { [K in Param]: string | number } // required terminal param\n : {}; // no params\n\n/** Collapses intersection types into a single object for cleaner IDE tooltips. */\ntype Prettify<T> = { [K in keyof T]: T[K] } & {};\n\n/** All named path parameters for a given route path literal. */\ntype RouteParams<Path extends string> = Prettify<ExtractRouteParams<Path>>;\n\n/** True when the path string contains at least one named parameter. */\ntype HasParams<Path extends string> =\n keyof RouteParams<Path> extends never ? false : true;\n\n/**\n * True when Search is the `never` type — meaning no search params were\n * declared on this route. Array wrapping avoids distributive evaluation.\n */\ntype SearchIsAbsent<Search> = [Search] extends [never] ? true : false;\n\n/**\n * True when Search has at least one required (non-optional) key.\n * { page: number; sort?: string } → true (page is required)\n * { sort?: string } → false (all optional)\n * never → false\n */\ntype HasRequiredSearchKeys<Search> =\n [Search] extends [never] ? false : {} extends Search ? false : true;\n\n// ---- Build option part types ------------------------------------------------\n\n/** Params portion of the build() argument. Empty object when path has no params. */\ntype ParamsPart<Path extends string> =\n HasParams<Path> extends true ? { params: RouteParams<Path> } : {};\n\n/**\n * Search portion of the build() argument.\n * Absent when Search is never.\n * Required when Search has at least one required key.\n * Optional when all Search keys are optional.\n */\ntype SearchPart<Search> =\n SearchIsAbsent<Search> extends true\n ? {}\n : HasRequiredSearchKeys<Search> extends true\n ? { search: Search }\n : { search?: Search };\n\n/** Full combined build() options, flattened for IDE readability. */\ntype BuildArgs<Path extends string, Search> = Prettify<\n ParamsPart<Path> & SearchPart<Search>\n>;\n\n/**\n * True when build() can be called with zero arguments:\n * no path params AND (no search OR all search keys are optional).\n */\ntype ArgIsOptional<Path extends string, Search> =\n HasParams<Path> extends true\n ? false // path params are always required\n : SearchIsAbsent<Search> extends true\n ? true // no search at all\n : HasRequiredSearchKeys<Search> extends true\n ? false // a required search key exists\n : true; // search exists but all keys are optional\n\n/**\n * True when BuildArgs produces an empty object — meaning build()\n * takes no arguments whatsoever (no params, no search).\n */\ntype BuildArgIsEmpty<Path extends string, Search> =\n HasParams<Path> extends false\n ? SearchIsAbsent<Search> extends true\n ? true\n : false\n : false;\n\n/**\n * The fully conditioned build() function signature.\n *\n * Four cases:\n * no params + no search → () => string\n * no params + all-optional search → (options?: { search?: S }) => string\n * params (+ optional search) → (options: { params: P; search?: S }) => string\n * required search key → (options: { search: S }) => string\n */\ntype BuildFn<Path extends string, Search> =\n BuildArgIsEmpty<Path, Search> extends true\n ? () => string\n : ArgIsOptional<Path, Search> extends true\n ? (options?: BuildArgs<Path, Search>) => string\n : (options: BuildArgs<Path, Search>) => string;\n\n// ============================================================================\n// PUBLIC TYPES\n// ============================================================================\n\n/** Optional metadata that can be attached to any route definition. */\nexport interface RouteMetadata {\n /**\n * Permission identifiers required to access this route.\n * Read via the `handle` property in your React Router config.\n *\n * @example\n * permissions: ['ADMIN', 'SUPER_ADMIN']\n */\n permissions?: string[];\n\n /**\n * Human-readable label used for breadcrumb navigation.\n * Read via the `handle` property in your React Router config.\n */\n breadcrumb?: string;\n\n /**\n * Document title for the page.\n * Read via the `handle` property in your React Router config.\n */\n title?: string;\n\n /**\n * Arbitrary custom metadata — analytics tags, loader IDs, feature flags, etc.\n * Read via the `handle` property in your React Router config.\n *\n * @example\n * meta: { analyticsId: 'user-detail-view', loaderKey: 'userLoader' }\n */\n meta?: Record<string, unknown>;\n}\n\n/**\n * The intermediate type returned by `defineRoute()`.\n * Consumed directly by `createRouteContract()` — you do not use this type\n * directly in application code.\n *\n * @internal\n */\nexport type RouteDefinition<\n Path extends string = string,\n Search = never,\n> = RouteMetadata & {\n /** The absolute path string for this route. */\n readonly path: Path;\n\n /**\n * Phantom type anchor — carries the Search type for downstream inference.\n * Always `undefined` at runtime. Do not read or write this directly.\n * @internal\n */\n readonly _search: Search;\n\n /**\n * Declares typed search parameters for this route. Chain immediately after\n * `defineRoute()`. The generic type argument is the only input — no value\n * needs to be passed.\n *\n * @example\n * defineRoute('/users').search<{ page: number; sort?: 'asc' | 'desc' }>()\n */\n search<S>(): RouteDefinition<Path, S>;\n};\n\n/**\n * A fully processed route node produced by `createRouteContract()`.\n * This is the type you interact with throughout the application.\n */\nexport type ProcessedRoute<\n Path extends string = string,\n Search = never,\n> = RouteMetadata & {\n /** The absolute path string — use this in `createBrowserRouter`. */\n readonly path: Path;\n\n /**\n * Phantom type anchor — used by `useTypedSearchParams` to infer `Search`.\n * Always `undefined` at runtime. Do not read or write this directly.\n * @internal\n */\n readonly _search: Search;\n\n /**\n * Builds a fully qualified URL for this route.\n *\n * TypeScript enforces at compile time that:\n * - `params` is provided and fully satisfied when the path has dynamic segments.\n * - `search` matches the exact shape declared via `.search<T>()`.\n * - No argument is needed for routes with neither params nor required search.\n */\n readonly build: BuildFn<Path, Search>;\n};\n\n/** The input shape `createRouteContract` accepts. */\ntype RouteTree = {\n [K: string]: RouteDefinition<string, any> | RouteTree;\n};\n\n/** Maps a RouteTree recursively to a tree of ProcessedRoutes. */\ntype ProcessedTree<T> = {\n [K in keyof T]: T[K] extends RouteDefinition<infer Path, infer Search>\n ? ProcessedRoute<Path, Search>\n : T[K] extends RouteTree\n ? ProcessedTree<T[K]>\n : never;\n};\n\n// ============================================================================\n// RUNTIME: TYPE GUARD\n// ============================================================================\n\n/**\n * Differentiates a RouteDefinition leaf from a plain route group object.\n * Checks for the three properties that only `defineRoute()` produces:\n * `path` (string), `_search` (phantom), and `search` (type-setting method).\n */\nfunction isRouteDefinition(node: unknown): node is RouteDefinition {\n if (node === null || typeof node !== \"object\") return false;\n const n = node as Record<string, unknown>;\n return (\n typeof n[\"path\"] === \"string\" &&\n \"_search\" in n &&\n typeof n[\"search\"] === \"function\"\n );\n}\n\n// ============================================================================\n// RUNTIME: defineRoute\n// ============================================================================\n\n/**\n * Defines a single route. Pass directly to `createRouteContract()`.\n *\n * The second argument is purely metadata — permissions, breadcrumbs, titles.\n * Chain `.search<SearchType>()` to declare typed search parameters for the route.\n *\n * **Use absolute paths.** Concatenating parent/child paths via template literals\n * causes TypeScript server slowdowns on large apps. Be explicit.\n *\n * @example\n * // Plain route — no params, no search\n * defineRoute('/dashboard', { breadcrumb: 'Home', title: 'Dashboard' })\n *\n * // Route with typed search params\n * defineRoute('/users', { permissions: ['ADMIN'] })\n * .search<{ page: number; sort?: 'asc' | 'desc' }>()\n *\n * // Route with path params and optional search\n * defineRoute('/users/:userId', { breadcrumb: 'User Detail' })\n * .search<{ tab?: 'profile' | 'settings' }>()\n *\n * // Route with path params, no search\n * defineRoute('/users/:userId/posts/:postId')\n */\nexport function defineRoute<Path extends string>(\n path: Path,\n config?: RouteMetadata,\n): RouteDefinition<Path, never> {\n const definition: RouteDefinition<Path, never> = {\n ...config,\n path,\n // Phantom anchor — undefined at runtime, typed as `never` for the base definition.\n // The search<S>() method changes this to `S` at the TypeScript level only.\n _search: undefined as unknown as never,\n search<S>(): RouteDefinition<Path, S> {\n // Pure type-level operation. The runtime object is returned unchanged.\n // TypeScript sees the return type as RouteDefinition<Path, S> and uses\n // that for all downstream generic inference.\n return definition as unknown as RouteDefinition<Path, S>;\n },\n };\n return definition;\n}\n\n// ============================================================================\n// RUNTIME: createRouteContract\n// ============================================================================\n\n/**\n * Processes a tree of `defineRoute()` definitions into a fully typed contract.\n *\n * Every route leaf gains a `build()` function whose signature is automatically\n * conditioned on the presence of path params and search params. Nested groups\n * are preserved as plain objects. All metadata (`path`, `permissions`,\n * `breadcrumb`, `title`, `meta`) flows through to the output unchanged.\n *\n * Call once at module level and export. Import `AppRoutes` wherever you need\n * to build a URL, wire up the router, or access route metadata.\n *\n * @example\n * // routes.ts\n * export const AppRoutes = createRouteContract({\n * auth: {\n * login: defineRoute('/auth/login').search<{ redirect?: string }>(),\n * register: defineRoute('/auth/register'),\n * },\n * dashboard: {\n * root: defineRoute('/dashboard', { breadcrumb: 'Home', title: 'Dashboard' }),\n * users: {\n * list: defineRoute('/dashboard/users', {\n * permissions: ['ADMIN'],\n * breadcrumb: 'Users',\n * }).search<{ page: number; sort?: 'asc' | 'desc' }>(),\n * detail: defineRoute('/dashboard/users/:userId', {\n * permissions: ['ADMIN'],\n * breadcrumb: 'User Detail',\n * }).search<{ tab?: 'profile' | 'settings' }>(),\n * },\n * },\n * });\n */\nexport function createRouteContract<T extends RouteTree>(\n tree: T,\n): ProcessedTree<T> {\n const result: Record<string, unknown> = {};\n\n for (const key in tree) {\n const node = tree[key];\n\n if (isRouteDefinition(node)) {\n // Strip the phantom _search and the type-only search() method.\n // They exist only for TypeScript — the processed node doesn't expose them.\n const {\n _search: _phantom,\n search: _searchFn,\n ...metadata\n } = node as RouteDefinition & Record<string, unknown>;\n\n result[key] = {\n ...metadata,\n // Preserve the phantom anchor on the ProcessedRoute for useTypedSearchParams.\n _search: undefined as unknown as never,\n\n build(\n options: {\n params?: Record<string, string | number>;\n search?: Record<string, unknown>;\n } = {},\n ): string {\n const { params, search } = options;\n\n // 1. Resolve path params via React Router's battle-tested generatePath.\n // Handles :param, :param?, and wildcard segments correctly.\n const pathname: string = params\n ? generatePath(\n node.path,\n Object.fromEntries(\n Object.entries(params).map(([k, v]) => [k, String(v)]),\n ),\n )\n : node.path;\n\n // 2. Serialize search params into a query string.\n // undefined and null values are silently dropped.\n if (search) {\n const defined = Object.entries(search).filter(\n ([, v]) => v !== undefined && v !== null,\n );\n\n if (defined.length > 0) {\n const qs = new URLSearchParams(\n defined.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `${pathname}?${qs}`;\n }\n }\n\n return pathname;\n },\n };\n } else {\n // Recurse into nested route groups — they are plain objects without\n // `path`, `_search`, or `search`, so isRouteDefinition returns false.\n result[key] = createRouteContract(node as RouteTree);\n }\n }\n\n return result as ProcessedTree<T>;\n}\n\n// ============================================================================\n// RUNTIME: useTypedSearchParams\n// ============================================================================\n\n/**\n * Returns the current URL search params typed to the shape declared on the\n * route, plus a type-safe setter and a clear function.\n *\n * Pass any processed route that was created with `.search<T>()`. TypeScript\n * infers `T` automatically — no generics needed at the call site.\n *\n * **`setSearch` merges** — it does not replace the entire query string.\n * Pass only the keys you want to change; everything else is preserved.\n * Set a key to `undefined` or `null` to remove it from the URL.\n *\n * ⚠️ **Runtime coercion note:** React Router's `useSearchParams` returns all\n * values as strings. If you declare `page: number`, `search.page` will be\n * the string `\"1\"` at runtime even though TypeScript types it as `number`.\n * Coerce where needed: `Number(search.page)`. This is a deliberate trade-off\n * that avoids requiring a runtime schema library.\n *\n * @example\n * // Inside the /dashboard/users page component\n * const { search, setSearch, clearSearch } =\n * useTypedSearchParams(AppRoutes.dashboard.users.list);\n *\n * // search.page is typed as `number | undefined`\n * // but is a string at runtime — coerce explicitly:\n * const page = Number(search.page ?? 1);\n *\n * setSearch({ page: 2 }) // keeps sort, q; updates page\n * setSearch({ page: 1, sort: 'asc' }) // updates page and sort; keeps q\n * setSearch({ sort: undefined }) // removes sort from the URL\n * clearSearch() // wipes all search params\n */\nexport function useTypedSearchParams<P extends string, S>(\n // Only used for TypeScript to infer S from ProcessedRoute<P, S>.\n // The runtime value is not read — the hook uses useSearchParams directly.\n _route: ProcessedRoute<P, S>,\n): {\n /** Current search params, typed as a partial of the declared search shape. */\n readonly search: Readonly<Partial<S>>;\n /**\n * Merges the given partial update into the current search params and\n * pushes a new URL entry. Set a key to `undefined` to remove it.\n */\n setSearch: (update: Partial<S>) => void;\n /** Removes all search parameters from the URL. */\n clearSearch: () => void;\n} {\n const [searchParams, setSearchParams] = useSearchParams();\n\n // Cast URLSearchParams entries to the declared type.\n // Values are strings at runtime — documented in the JSDoc above.\n const search = Object.fromEntries(searchParams.entries()) as Partial<S>;\n\n function setSearch(update: Partial<S>): void {\n setSearchParams((prev) => {\n const next: Record<string, string> = Object.fromEntries(prev.entries());\n\n for (const [k, v] of Object.entries(\n update as Record<string, unknown>,\n )) {\n if (v === undefined || v === null) {\n delete next[k];\n } else {\n next[k] = String(v);\n }\n }\n\n return next;\n });\n }\n\n function clearSearch(): void {\n setSearchParams({});\n }\n\n return { search, setSearch, clearSearch } as const;\n}\n","import { type ReactNode, useCallback, useState } from \"react\";\n\nexport type VSAlertVariant = \"success\" | \"info\" | \"error\";\n\nexport interface VSAlertState {\n message: ReactNode | string;\n type: VSAlertVariant;\n isVisible: boolean;\n}\n\n/**\n * Manages alert/toast message state with optional auto-hide.\n *\n * @param autoHideDuration - ms before alert hides automatically. Pass 0 to disable. Default: 3000\n *\n * @example\n * const { alert, showAlert, hideAlert } = useAlertMessage();\n * showAlert('Saved successfully!', 'success');\n * showAlert(<b>Something went wrong</b>, 'error');\n */\nexport function useAlertMessage(autoHideDuration = 3000) {\n const [alert, setAlert] = useState<VSAlertState>({\n message: null,\n type: \"info\",\n isVisible: false,\n });\n\n const showAlert = useCallback(\n (message: ReactNode | string, type: VSAlertVariant = \"info\") => {\n setAlert({ message, type, isVisible: true });\n if (autoHideDuration) {\n setTimeout(() => {\n setAlert((prev) => ({ ...prev, isVisible: false }));\n }, autoHideDuration);\n }\n },\n [autoHideDuration]\n );\n\n const hideAlert = useCallback(() => {\n setAlert((prev) => ({ ...prev, isVisible: false }));\n }, []);\n\n return { alert, showAlert, hideAlert };\n}\n","import { useCallback, useMemo, useState } from \"react\";\nimport { catchError } from \"@void-snippets/core\";\n\nexport type VSAsyncStatus = \"idle\" | \"pending\" | \"success\" | \"error\";\n\ninterface VSAsyncState<T> {\n data: T | null;\n status: VSAsyncStatus;\n error: Error | null;\n}\n\nexport interface VSUseAsyncStateReturn<T> extends VSAsyncState<T> {\n isLoading: boolean;\n isSuccess: boolean;\n isError: boolean;\n\n setData: (data: T | null) => void;\n setError: (error: Error | null) => void;\n reset: () => void;\n\n /**\n * Executes an async function, updates state, and returns a [err, data] tuple.\n * Allows immediate result handling without try/catch.\n *\n * @example\n * const [err, data] = await execute(() => ContactsApis.create(payload));\n * if (err) return showAlert(err.message, 'error');\n * showAlert('Created!', 'success');\n */\n execute: (\n asyncFn: () => Promise<T>,\n options?: {\n onSuccess?: (data: T) => void;\n onError?: (error: Error) => void;\n }\n ) => Promise<[Error, null] | [null, T]>;\n}\n\n/**\n * Generic async state machine — tracks data, status, and error for any async operation.\n * Pair with any async function: API calls, file reads, timers, etc.\n *\n * @param initialData - Optional initial data value. Default: null\n *\n * @example\n * const { data, isLoading, isError, execute } = useAsyncState<User>();\n *\n * const handleSubmit = async () => {\n * const [err, user] = await execute(() => fetchUser(id));\n * if (err) return;\n * console.log(user.name);\n * };\n */\nexport function useAsyncState<T>(\n initialData: T | null = null\n): VSUseAsyncStateReturn<T> {\n const [state, setState] = useState<VSAsyncState<T>>({\n data: initialData,\n status: \"idle\",\n error: null,\n });\n\n const setData = useCallback((data: T | null) => {\n setState({ data, status: \"success\", error: null });\n }, []);\n\n const setError = useCallback((error: Error | null) => {\n setState((prev) => ({ ...prev, error, status: \"error\" }));\n }, []);\n\n const reset = useCallback(() => {\n setState({ data: initialData, status: \"idle\", error: null });\n }, [initialData]);\n\n const execute = useCallback(\n async (\n asyncFn: () => Promise<T>,\n options?: {\n onSuccess?: (data: T) => void;\n onError?: (error: Error) => void;\n }\n ): Promise<[Error, null] | [null, T]> => {\n setState((prev) => ({ ...prev, status: \"pending\", error: null }));\n\n const [err, res] = await catchError(asyncFn());\n\n if (err) {\n setState((prev) => ({ ...prev, status: \"error\", error: err }));\n options?.onError?.(err);\n return [err, null];\n }\n\n setState({ data: res as T, status: \"success\", error: null });\n options?.onSuccess?.(res as T);\n return [null, res as T];\n },\n []\n );\n\n const flags = useMemo(\n () => ({\n isLoading: state.status === \"pending\",\n isSuccess: state.status === \"success\",\n isError: state.status === \"error\",\n }),\n [state.status]\n );\n\n return { ...state, ...flags, setData, setError, reset, execute };\n}\n","import { useEffect, useState } from \"react\";\n\n/**\n * Tracks elapsed time from a given start timestamp — useful for call durations,\n * countdowns, or any elapsed-time display.\n *\n * @param startedAt - Unix timestamp in ms (e.g. Date.now()). Pass null/undefined to reset.\n * @returns Formatted duration string \"MM:SS\"\n *\n * @example\n * const duration = useCallTimer(call.startedAt);\n * // duration → \"02:45\"\n *\n * // Reset when no active call\n * const duration = useCallTimer(activeCall ? activeCall.startedAt : null);\n */\nexport function useCallTimer(startedAt?: number | null): string {\n const [duration, setDuration] = useState(\"00:00\");\n\n useEffect(() => {\n if (!startedAt) {\n setDuration(\"00:00\");\n return;\n }\n\n const interval = setInterval(() => {\n const diffInSeconds = Math.floor((Date.now() - startedAt) / 1000);\n const minutes = Math.floor(diffInSeconds / 60).toString().padStart(2, \"0\");\n const seconds = (diffInSeconds % 60).toString().padStart(2, \"0\");\n setDuration(`${minutes}:${seconds}`);\n }, 1000);\n\n return () => clearInterval(interval);\n }, [startedAt]);\n\n return duration;\n}\n","import { useCallback, useState } from \"react\";\n\nexport interface VSModalReturn<T> {\n isOpen: boolean;\n data: T | null;\n isLoading: boolean;\n openCreateModal: () => void;\n openEditModal: (editData: T) => void;\n setLoading: (loading: boolean) => void;\n closeModal: () => void;\n setModal: (open: boolean, editData?: T | null) => void;\n}\n\n/**\n * Manages modal open/close state with optional data payload and loading state.\n * Works for both create and edit modals — pass data to distinguish the mode.\n *\n * @typeParam T - The type of data the modal operates on (e.g. a Contact, User, etc.)\n *\n * @example\n * const modal = useModal<Contact.Base>();\n *\n * modal.openCreateModal(); // data → null (create mode)\n * modal.openEditModal(contact); // data → contact (edit mode)\n *\n * if (modal.data) {\n * // Edit mode — modal.data is Contact.Base\n * } else {\n * // Create mode\n * }\n */\nexport function useModal<T = unknown>(): VSModalReturn<T> {\n const [isOpen, setIsOpen] = useState(false);\n const [data, setData] = useState<T | null>(null);\n const [isLoading, setIsLoading] = useState(false);\n\n const openCreateModal = useCallback(() => {\n setIsOpen(true);\n setData(null);\n }, []);\n\n const openEditModal = useCallback((editData: T) => {\n setIsOpen(true);\n setData(editData);\n }, []);\n\n const setLoading = useCallback((loading: boolean) => {\n setIsLoading(loading);\n }, []);\n\n const closeModal = useCallback(() => {\n setIsOpen(false);\n setData(null);\n }, []);\n\n const setModal = useCallback((open: boolean, editData?: T | null) => {\n setIsOpen(open);\n setData(editData ?? null);\n }, []);\n\n return {\n isOpen,\n data,\n isLoading,\n openCreateModal,\n openEditModal,\n setLoading,\n closeModal,\n setModal,\n };\n}\n","import { useCallback, useState } from \"react\";\nimport type { VSQueryParams } from \"@void-snippets/core\";\n\nexport interface VSPaginationReturn {\n page: number;\n limit: number;\n onPaginationChange: (newPage: number, newLimit: number) => void;\n resetPagination: () => void;\n setPage: (page: number) => void;\n setLimit: (limit: number) => void;\n /** Ready-to-use query params object — pass directly to useList() */\n queryParams: VSQueryParams;\n}\n\n/**\n * Manages pagination state and produces a ready-to-use queryParams object\n * compatible with createResourceHooks' useList() and useInfinite().\n *\n * @param initialPage - Starting page. Default: 1\n * @param initialLimit - Items per page. Default: 10\n *\n * @example\n * const { queryParams, onPaginationChange } = usePagination(1, 20);\n *\n * const { list, isLoading } = contactHooks.useList(queryParams);\n *\n * <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />\n */\nexport function usePagination(\n initialPage = 1,\n initialLimit = 10\n): VSPaginationReturn {\n const [page, setPage] = useState(initialPage);\n const [limit, setLimit] = useState(initialLimit);\n\n const onPaginationChange = useCallback(\n (newPage: number, newLimit: number) => {\n setPage(newPage);\n setLimit(newLimit);\n },\n []\n );\n\n const resetPagination = useCallback(() => {\n setPage(1);\n }, []);\n\n return {\n page,\n limit,\n onPaginationChange,\n resetPagination,\n setPage,\n setLimit,\n queryParams: { page, limit },\n };\n}\n"],"mappings":"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","names":["useCallback","useState","useCallback","useState","useEffect","useState","useCallback","useState","useCallback","useState"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@void-snippets/react",
3
- "version": "0.3.0",
4
- "description": "TanStack Query resource hooks factory and general-purpose React hooks",
5
- "homepage": "https://github.com/shahtirthhh/void-snippets",
3
+ "version": "0.6.1",
4
+ "description": "TanStack Query resource hooks factory, Socket.IO hooks factory, type-safe routing contract, and general-purpose React hooks",
5
+ "homepage": "https://void-snippets.vercel.app",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/shahtirthhh/void-snippets.git",
@@ -30,7 +30,16 @@
30
30
  "react-query",
31
31
  "hooks",
32
32
  "api",
33
+ "socket.io",
34
+ "websocket",
35
+ "routing",
36
+ "react-router",
37
+ "type-safe",
33
38
  "void-snippets",
39
+ "createResourceHooks",
40
+ "createSocketHooks",
41
+ "createRouteContract",
42
+ "useTypedSearchParams",
34
43
  "useModal",
35
44
  "usePagination",
36
45
  "useAsyncState",
@@ -40,15 +49,27 @@
40
49
  "license": "MIT",
41
50
  "peerDependencies": {
42
51
  "react": ">=17.0.0",
43
- "@void-snippets/client": ">=0.1.0"
52
+ "@void-snippets/client": ">=0.1.0",
53
+ "socket.io-client": ">=4.6.0",
54
+ "react-router": ">=7.0.0"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "socket.io-client": {
58
+ "optional": true
59
+ },
60
+ "react-router": {
61
+ "optional": true
62
+ }
44
63
  },
45
64
  "dependencies": {
46
65
  "@tanstack/react-query": "^5.0.0",
47
- "@void-snippets/core": "0.3.0"
66
+ "@void-snippets/core": "0.3.1"
48
67
  },
49
68
  "devDependencies": {
50
69
  "@types/react": "^18.0.0",
51
70
  "react": "^18.0.0",
71
+ "react-router": "^7.0.0",
72
+ "socket.io-client": "^4.7.0",
52
73
  "tsup": "^8.0.0",
53
74
  "typescript": "^5.4.0"
54
75
  },
package/README.md DELETED
@@ -1,192 +0,0 @@
1
- # @void-snippets/react
2
-
3
- TanStack Query v5 resource hooks factory + general-purpose React hooks. All fully typed, zero boilerplate.
4
-
5
- ## Installation
6
-
7
- ```bash
8
- npm install @void-snippets/react @void-snippets/client @tanstack/react-query axios
9
- ```
10
-
11
- ---
12
-
13
- ## Setup
14
-
15
- ### Configure axios once
16
-
17
- ```ts
18
- import axios from 'axios';
19
- import { configure } from '@void-snippets/client';
20
- configure(axios.create({ baseURL: 'https://api.example.com' }));
21
- ```
22
-
23
- ### Wrap your app with QueryClientProvider
24
-
25
- ```tsx
26
- import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
27
- const queryClient = new QueryClient();
28
- function App() {
29
- return <QueryClientProvider client={queryClient}><YourApp /></QueryClientProvider>;
30
- }
31
- ```
32
-
33
- ---
34
-
35
- ## Resource Hooks — `createResourceHooks`
36
-
37
- Define a service once, get all hooks free — fully typed, no generics needed.
38
-
39
- ```ts
40
- // contacts.api.ts
41
- export class ContactsApiService extends ResourceService<
42
- Contact.Id, Contact.Base, Contact.WithCreatedBy,
43
- Contact.Apis.CreatePayload, Contact.Apis.UpdatePayload
44
- > {
45
- constructor() { super('/contacts'); }
46
- }
47
- export const ContactsApis = new ContactsApiService();
48
-
49
- // contacts.hooks.ts
50
- export const contactHooks = createResourceHooks('contacts', ContactsApis);
51
- ```
52
-
53
- ### `useList(params?)`
54
-
55
- ```tsx
56
- const { list, isLoading, pagination, error, invalidate } =
57
- contactHooks.useList({ page: 1, limit: 20 });
58
- // list is typed as Contact.Base[] ✅
59
- ```
60
-
61
- | Key | Type |
62
- |---|---|
63
- | `list` | `TBase[]` |
64
- | `pagination` | `VSPagination` |
65
- | `isLoading` | `boolean` |
66
- | `error` | `Error \| null` |
67
- | `invalidate` | `() => void` |
68
-
69
- ### `useGet(id, staleTime?)`
70
-
71
- ```tsx
72
- const { item, isLoading, error, refetch } = contactHooks.useGet(id);
73
- // item is typed as Contact.WithCreatedBy ✅
74
- ```
75
-
76
- ### `useMutations()`
77
-
78
- ```tsx
79
- const { create, update, remove } = contactHooks.useMutations();
80
-
81
- create.mutate({ name: 'John' });
82
- update.mutate({ _id: id, payload: { name: 'Jane' } });
83
- remove.mutate(id);
84
- ```
85
-
86
- ### `useInfinite(params?)`
87
-
88
- ```tsx
89
- const { data, fetchNextPage, hasNextPage } = contactHooks.useInfinite({ limit: 20 });
90
- const all = data?.pages.flatMap(p => p.items) ?? [];
91
- ```
92
-
93
- ### Custom API shapes
94
-
95
- ```ts
96
- export const contactHooks = createResourceHooks('contacts', ContactsApis, {
97
- adapters: {
98
- fromList: (raw) => ({
99
- items: raw.results,
100
- pagination: {
101
- page: raw.meta.page,
102
- limit: raw.meta.perPage,
103
- totalPages: raw.meta.lastPage,
104
- totalDocuments: raw.meta.total,
105
- },
106
- }),
107
- fromSingle: (raw) => raw.payload,
108
- },
109
- });
110
- ```
111
-
112
- ---
113
-
114
- ## General-Purpose Hooks
115
-
116
- ### `useAlertMessage(autoHideDuration?)`
117
-
118
- ```tsx
119
- const { alert, showAlert, hideAlert } = useAlertMessage(3000);
120
-
121
- showAlert('Saved!', 'success');
122
- showAlert('Something went wrong', 'error');
123
- showAlert(<b>Custom JSX</b>, 'info');
124
- // alert.isVisible, alert.message, alert.type
125
- ```
126
-
127
- Variants: `"success" | "info" | "error"`
128
-
129
- ---
130
-
131
- ### `useAsyncState<T>(initialData?)`
132
-
133
- ```tsx
134
- const { data, isLoading, isError, execute } = useAsyncState<User>();
135
-
136
- const [err, user] = await execute(() => fetchUser(id), {
137
- onSuccess: (u) => showAlert(`Welcome ${u.name}`, 'success'),
138
- onError: (e) => showAlert(e.message, 'error'),
139
- });
140
- ```
141
-
142
- Status values: `"idle" | "pending" | "success" | "error"`
143
-
144
- ---
145
-
146
- ### `useCallTimer(startedAt?)`
147
-
148
- ```tsx
149
- const duration = useCallTimer(call.startedAt); // "02:45"
150
- const duration = useCallTimer(null); // "00:00"
151
- ```
152
-
153
- ---
154
-
155
- ### `useModal<T>()`
156
-
157
- ```tsx
158
- const modal = useModal<Contact.Base>();
159
-
160
- modal.openCreateModal(); // data → null
161
- modal.openEditModal(contact); // data → contact
162
-
163
- if (modal.data) { /* edit mode */ } else { /* create mode */ }
164
- ```
165
-
166
- ---
167
-
168
- ### `usePagination(initialPage?, initialLimit?)`
169
-
170
- ```tsx
171
- const { queryParams, onPaginationChange } = usePagination(1, 20);
172
-
173
- // Wire directly to useList
174
- const { list } = contactHooks.useList(queryParams);
175
-
176
- // Wire to your pagination UI
177
- <Pagination onChange={onPaginationChange} total={pagination.totalDocuments} />
178
- ```
179
-
180
- ---
181
-
182
- ## Part of the `@void-snippets` ecosystem
183
-
184
- | Package | Description |
185
- |---|---|
186
- | `@void-snippets/core` | Shared types and utilities |
187
- | `@void-snippets/client` | Framework-agnostic CRUD resource service (axios) |
188
- | `@void-snippets/react` | This package |
189
-
190
- ## License
191
-
192
- MIT