From 6b06e04c4db7c9eadc47fce72c29f3cf21e78285 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Wed, 12 Jun 2024 13:03:59 -0700 Subject: [PATCH] Create `ZQuery` package for managing data in Zustand (#1199) * Create ZQuery lib * Remove unused code * Tweak syntax and fix performance * Refactor a little * Change slice name * Fix API to no longer use useStore * Simplify a bit * Make it possible to pass args to fetch() * Remove fetch args; go back to exposing hooks * Fix hooks * Use ZQuery for fetching unclaimed swaps * Coerce types for now * Rework a bit to simplify how the store is used * Remove unnecessary passing around of revalidate() function * Document the parameters * Add some spacing * Reorder destructuring * Add a ton more docs * Clarify docs a bit * Rename generic variable * Add more docs and accommodate more store typings * Take advantage of typings fixes * Move ZQuery to a package * fix type of use hook * Sort unclaimed swaps in fetcher; simplify component * Export the type * Install vitest * Write first tests * Significantly refactor ZQuery to handle streaming responses * Fix typing issues * Fix tests * Fix remaining type issues * Use updated ZQuery APi in UnclaimedSwaps and DutchAuction slices * Remove dupe code * Fix type * Clean up a bit; add explanatory comment * Remove duplicate type * Fix import * Support fetch args * Fix complaint about non-extensible data object * Use useAuctionInfos * Fix types again * Fix auction metadata * Fix QueryLatestStateButton * Remove unused DataTypeInState type * Require stream to be a function that returns the desired type * Fix type * Fix type * Handle errors in the stream * Fix circular reference issue * Convert status slice to use ZQuery * Simplify type * Fix import * Fix more type issues; support referencing counting * Rename var * Stop streaming results if aborted * Update docs * Add comment * Use correct source for latestKnownBlockHeight in TokenSwapInput * Remove unused abortController * Rename fetch types * Remove unneeded named types * Remove fetch options for now * Remove unnecessary optional checks * Update doc * Make exponents optional * Remove outdated comment * Add changeset --- .changeset/ninety-pandas-think.md | 8 + apps/minifront/package.json | 1 + apps/minifront/src/components/Status.tsx | 14 - .../components/header/sync-status-section.tsx | 18 +- apps/minifront/src/components/layout.tsx | 2 - .../get-filtered-auction-infos.test.ts | 2 +- .../get-filtered-auction-infos.ts | 3 +- .../components/swap/auction-list/helpers.ts | 17 +- .../components/swap/auction-list/index.tsx | 37 +-- .../query-latest-state-button.tsx | 7 +- .../swap/swap-form/token-swap-input.tsx | 5 +- .../src/components/swap/swap-loader.ts | 40 +-- .../src/components/swap/unclaimed-swaps.tsx | 33 +-- apps/minifront/src/fetchers/auction-infos.ts | 48 ++++ apps/minifront/src/fetchers/status.ts | 24 ++ .../minifront/src/fetchers/unclaimed-swaps.ts | 35 ++- apps/minifront/src/main.tsx | 6 + apps/minifront/src/state/status.ts | 59 ++-- .../src/state/swap/dutch-auction/index.ts | 106 ++----- apps/minifront/src/state/unclaimed-swaps.ts | 96 ++++--- packages/getters/src/dutch-auction.ts | 8 + .../expanded-details/index.tsx | 4 +- packages/zquery/eslint.config.mjs | 13 + packages/zquery/package.json | 15 + packages/zquery/src/index.test.ts | 56 ++++ packages/zquery/src/index.ts | 261 ++++++++++++++++++ packages/zquery/src/types.ts | 190 +++++++++++++ packages/zquery/tsconfig.json | 5 + pnpm-lock.yaml | 106 ++++--- 29 files changed, 893 insertions(+), 326 deletions(-) create mode 100644 .changeset/ninety-pandas-think.md delete mode 100644 apps/minifront/src/components/Status.tsx create mode 100644 apps/minifront/src/fetchers/auction-infos.ts create mode 100644 apps/minifront/src/fetchers/status.ts create mode 100644 packages/zquery/eslint.config.mjs create mode 100644 packages/zquery/package.json create mode 100644 packages/zquery/src/index.test.ts create mode 100644 packages/zquery/src/index.ts create mode 100644 packages/zquery/src/types.ts create mode 100644 packages/zquery/tsconfig.json diff --git a/.changeset/ninety-pandas-think.md b/.changeset/ninety-pandas-think.md new file mode 100644 index 00000000..88a003c6 --- /dev/null +++ b/.changeset/ninety-pandas-think.md @@ -0,0 +1,8 @@ +--- +'@penumbra-zone/zquery': major +'@penumbra-zone/getters': minor +'minifront': patch +'@penumbra-zone/ui': patch +--- + +Introduce ZQuery package and use throughout minifront diff --git a/apps/minifront/package.json b/apps/minifront/package.json index ee73dc91..931d12f6 100644 --- a/apps/minifront/package.json +++ b/apps/minifront/package.json @@ -31,6 +31,7 @@ "@penumbra-zone/transport-dom": "workspace:*", "@penumbra-zone/types": "workspace:*", "@penumbra-zone/ui": "workspace:*", + "@penumbra-zone/zquery": "workspace:*", "@radix-ui/react-icons": "^1.3.0", "@tanstack/react-query": "4.36.1", "bech32": "^2.0.0", diff --git a/apps/minifront/src/components/Status.tsx b/apps/minifront/src/components/Status.tsx deleted file mode 100644 index e6645358..00000000 --- a/apps/minifront/src/components/Status.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { useEffect } from 'react'; -import { useStore } from '../state'; - -/** - * A component that simply keeps the Zustand state up to date with the latest - * output of `viewClient.status()`. - */ -export const Status = () => { - const start = useStore(state => state.status.start); - - useEffect(() => void start(), [start]); - - return null; -}; diff --git a/apps/minifront/src/components/header/sync-status-section.tsx b/apps/minifront/src/components/header/sync-status-section.tsx index 1e48a4a4..9522fed3 100644 --- a/apps/minifront/src/components/header/sync-status-section.tsx +++ b/apps/minifront/src/components/header/sync-status-section.tsx @@ -1,22 +1,16 @@ import { CondensedBlockSyncStatus } from '@penumbra-zone/ui/components/ui/block-sync-status/condensed'; -import { AllSlices } from '../../state'; -import { useStoreShallow } from '../../utils/use-store-shallow'; - -const syncStatusSectionSelector = (state: AllSlices) => ({ - fullSyncHeight: state.status.fullSyncHeight, - latestKnownBlockHeight: state.status.latestKnownBlockHeight, - error: state.status.error, -}); +import { useStatus } from '../../state/status'; export const SyncStatusSection = () => { - const { fullSyncHeight, latestKnownBlockHeight, error } = - useStoreShallow(syncStatusSectionSelector); + const { data, error } = useStatus(); return (
diff --git a/apps/minifront/src/components/layout.tsx b/apps/minifront/src/components/layout.tsx index 91cc05c1..2dd0ccff 100644 --- a/apps/minifront/src/components/layout.tsx +++ b/apps/minifront/src/components/layout.tsx @@ -7,7 +7,6 @@ import '@penumbra-zone/ui/styles/globals.css'; import { getChainId } from '../fetchers/chain-id'; import { useEffect, useState } from 'react'; import { TestnetBanner } from '@penumbra-zone/ui/components/ui/testnet-banner'; -import { Status } from './Status'; import { MotionConfig } from 'framer-motion'; export const Layout = () => { @@ -19,7 +18,6 @@ export const Layout = () => { return ( -
diff --git a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts index 00933197..fe129123 100644 --- a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts +++ b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest'; import { getFilteredAuctionInfos } from './get-filtered-auction-infos'; -import { AuctionInfo } from '../../../state/swap/dutch-auction'; import { AuctionId, DutchAuction, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1/auction_pb'; +import { AuctionInfo } from '../../../fetchers/auction-infos'; const MOCK_AUCTION_1 = new DutchAuction({ description: { diff --git a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts index 000f4808..49df7f57 100644 --- a/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts +++ b/apps/minifront/src/components/swap/auction-list/get-filtered-auction-infos.ts @@ -1,4 +1,5 @@ -import { AuctionInfo, Filter } from '../../../state/swap/dutch-auction'; +import { AuctionInfo } from '../../../fetchers/auction-infos'; +import { Filter } from '../../../state/swap/dutch-auction'; type FilterMatchableAuctionInfo = AuctionInfo & { auction: { diff --git a/apps/minifront/src/components/swap/auction-list/helpers.ts b/apps/minifront/src/components/swap/auction-list/helpers.ts index 27f19b7e..d26f1955 100644 --- a/apps/minifront/src/components/swap/auction-list/helpers.ts +++ b/apps/minifront/src/components/swap/auction-list/helpers.ts @@ -1,9 +1,5 @@ -import { - AssetId, - Metadata, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { AuctionInfo, Filter } from '../../../state/swap/dutch-auction'; -import { bech32mAssetId } from '@penumbra-zone/bech32m/passet'; +import { AuctionInfo } from '../../../fetchers/auction-infos'; +import { Filter } from '../../../state/swap/dutch-auction'; const byStartHeight = (direction: 'ascending' | 'descending') => (a: AuctionInfo, b: AuctionInfo) => { @@ -19,12 +15,3 @@ export const SORT_FUNCTIONS: Record active: byStartHeight('descending'), upcoming: byStartHeight('ascending'), }; - -export const getMetadata = (metadataByAssetId: Record, assetId?: AssetId) => { - let metadata: Metadata | undefined; - if (assetId && (metadata = metadataByAssetId[bech32mAssetId(assetId)])) { - return metadata; - } - - return new Metadata({ penumbraAssetId: assetId }); -}; diff --git a/apps/minifront/src/components/swap/auction-list/index.tsx b/apps/minifront/src/components/swap/auction-list/index.tsx index 05b8e7b3..4e1b8af4 100644 --- a/apps/minifront/src/components/swap/auction-list/index.tsx +++ b/apps/minifront/src/components/swap/auction-list/index.tsx @@ -10,12 +10,11 @@ import { SegmentedPicker } from '@penumbra-zone/ui/components/ui/segmented-picke import { useMemo } from 'react'; import { getFilteredAuctionInfos } from './get-filtered-auction-infos'; import { LayoutGroup, motion } from 'framer-motion'; -import { SORT_FUNCTIONS, getMetadata } from './helpers'; +import { SORT_FUNCTIONS } from './helpers'; +import { useAuctionInfos } from '../../../state/swap/dutch-auction'; +import { useStatus } from '../../../state/status'; const auctionListSelector = (state: AllSlices) => ({ - auctionInfos: state.swap.dutchAuction.auctionInfos, - metadataByAssetId: state.swap.dutchAuction.metadataByAssetId, - fullSyncHeight: state.status.fullSyncHeight, endAuction: state.swap.dutchAuction.endAuction, withdraw: state.swap.dutchAuction.withdraw, filter: state.swap.dutchAuction.filter, @@ -39,22 +38,16 @@ const getButtonProps = ( }; export const AuctionList = () => { - const { - auctionInfos, - metadataByAssetId, - fullSyncHeight, - endAuction, - withdraw, - filter, - setFilter, - } = useStoreShallow(auctionListSelector); + const auctionInfos = useAuctionInfos(); + const { endAuction, withdraw, filter, setFilter } = useStoreShallow(auctionListSelector); + const { data: status } = useStatus(); const filteredAuctionInfos = useMemo( () => - [...getFilteredAuctionInfos(auctionInfos, filter, fullSyncHeight)].sort( + [...getFilteredAuctionInfos(auctionInfos.data ?? [], filter, status?.fullSyncHeight)].sort( SORT_FUNCTIONS[filter], ), - [auctionInfos, filter, fullSyncHeight], + [auctionInfos, filter, status?.fullSyncHeight], ); return ( @@ -63,7 +56,7 @@ export const AuctionList = () => { My Auctions - {!!auctionInfos.length && } + {!!auctionInfos.data?.length && } { { - const loadAuctionInfos = useStore(state => state.swap.dutchAuction.loadAuctionInfos); - const handleClick = () => void loadAuctionInfos(true); + const revalidate = useRevalidateAuctionInfos(); return ( revalidate({ queryLatestState: true })} aria-label='Get the current auction reserves (makes a request to a fullnode)' >
diff --git a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx b/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx index 2790303f..96ad94ad 100644 --- a/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx +++ b/apps/minifront/src/components/swap/swap-form/token-swap-input.tsx @@ -24,6 +24,7 @@ import { import { AssetSelector } from '../../shared/asset-selector'; import BalanceSelector from '../../shared/balance-selector'; import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1/num_pb'; +import { useStatus } from '../../../state/status'; const isValidAmount = (amount: string, assetIn?: BalancesResponse) => Number(amount) >= 0 && (!assetIn || !amountMoreThanBalance(assetIn, amount)); @@ -61,7 +62,6 @@ const tokenSwapInputSelector = (state: AllSlices) => ({ setAmount: state.swap.setAmount, balancesResponses: state.swap.balancesResponses, priceHistory: state.swap.priceHistory, - latestKnownBlockHeight: state.status.latestKnownBlockHeight, assetOutBalance: assetOutBalanceSelector(state), }); @@ -72,6 +72,8 @@ const tokenSwapInputSelector = (state: AllSlices) => ({ * amount. */ export const TokenSwapInput = () => { + const status = useStatus(); + const latestKnownBlockHeight = status.data?.latestKnownBlockHeight ?? 0n; const { swappableAssets, amount, @@ -82,7 +84,6 @@ export const TokenSwapInput = () => { setAssetOut, balancesResponses, priceHistory, - latestKnownBlockHeight = 0n, assetOutBalance, } = useStoreShallow(tokenSwapInputSelector); diff --git a/apps/minifront/src/components/swap/swap-loader.ts b/apps/minifront/src/components/swap/swap-loader.ts index 78be4764..9adc5fe3 100644 --- a/apps/minifront/src/components/swap/swap-loader.ts +++ b/apps/minifront/src/components/swap/swap-loader.ts @@ -3,10 +3,6 @@ import { useStore } from '../../state'; import { abortLoader } from '../../abort-loader'; import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { fetchUnclaimedSwaps } from '../../fetchers/unclaimed-swaps'; -import { viewClient } from '../../clients'; -import { getSwapAsset1, getSwapAsset2 } from '@penumbra-zone/getters/swap-record'; -import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; import { getSwappableBalancesResponses, isSwappable } from './helpers'; import { getAllAssets } from '../../fetchers/assets'; @@ -30,44 +26,14 @@ const getAndSetDefaultAssetBalances = async (swappableAssets: Metadata[]) => { return balancesResponses; }; -const fetchMetadataForSwap = async (swap: SwapRecord): Promise => { - const assetId1 = getSwapAsset1(swap); - const assetId2 = getSwapAsset2(swap); - - const [{ denomMetadata: asset1Metadata }, { denomMetadata: asset2Metadata }] = await Promise.all([ - viewClient.assetMetadataById({ assetId: assetId1 }), - viewClient.assetMetadataById({ assetId: assetId2 }), - ]); - - return { - swap, - // If no metadata, uses assetId for asset icon display - asset1: asset1Metadata - ? asset1Metadata - : new Metadata({ display: uint8ArrayToBase64(assetId1.inner) }), - asset2: asset2Metadata - ? asset2Metadata - : new Metadata({ display: uint8ArrayToBase64(assetId2.inner) }), - }; -}; - -export const unclaimedSwapsWithMetadata = async (): Promise => { - const unclaimedSwaps = await fetchUnclaimedSwaps(); - return Promise.all(unclaimedSwaps.map(fetchMetadataForSwap)); -}; - -export const SwapLoader: LoaderFunction = async (): Promise => { +export const SwapLoader: LoaderFunction = async (): Promise => { await abortLoader(); const assets = await getAllAssets(); const swappableAssets = assets.filter(isSwappable); - const [balancesResponses, unclaimedSwaps] = await Promise.all([ - getAndSetDefaultAssetBalances(swappableAssets), - unclaimedSwapsWithMetadata(), - ]); + const balancesResponses = await getAndSetDefaultAssetBalances(swappableAssets); useStore.getState().swap.setBalancesResponses(balancesResponses); useStore.getState().swap.setSwappableAssets(swappableAssets); - void useStore.getState().swap.dutchAuction.loadAuctionInfos(); - return unclaimedSwaps; + return null; }; diff --git a/apps/minifront/src/components/swap/unclaimed-swaps.tsx b/apps/minifront/src/components/swap/unclaimed-swaps.tsx index 50174ab2..573c5e3b 100644 --- a/apps/minifront/src/components/swap/unclaimed-swaps.tsx +++ b/apps/minifront/src/components/swap/unclaimed-swaps.tsx @@ -1,35 +1,28 @@ import { Button } from '@penumbra-zone/ui/components/ui/button'; import { Card } from '@penumbra-zone/ui/components/ui/card'; -import { useLoaderData, useRevalidator } from 'react-router-dom'; -import { SwapLoaderResponse, UnclaimedSwapsWithMetadata } from './swap-loader'; import { AssetIcon } from '@penumbra-zone/ui/components/ui/tx/view/asset-icon'; -import { useStore } from '../../state'; -import { unclaimedSwapsSelector } from '../../state/unclaimed-swaps'; +import { AllSlices } from '../../state'; +import { useUnclaimedSwaps } from '../../state/unclaimed-swaps'; import { getSwapRecordCommitment } from '@penumbra-zone/getters/swap-record'; import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; import { GradientHeader } from '@penumbra-zone/ui/components/ui/gradient-header'; +import { useStoreShallow } from '../../utils/use-store-shallow'; + +const unclaimedSwapsSelector = (state: AllSlices) => ({ + claimSwap: state.unclaimedSwaps.claimSwap, + isInProgress: state.unclaimedSwaps.isInProgress, +}); export const UnclaimedSwaps = () => { - const unclaimedSwaps = useLoaderData() as SwapLoaderResponse; + const unclaimedSwaps = useUnclaimedSwaps(); + const { claimSwap, isInProgress } = useStoreShallow(unclaimedSwapsSelector); - const sortedUnclaimedSwaps = unclaimedSwaps.sort( - (a, b) => Number(b.swap.outputData?.height) - Number(a.swap.outputData?.height), - ); - return !unclaimedSwaps.length ? ( + return !unclaimedSwaps.data?.length ? (
) : ( - <_UnclaimedSwaps unclaimedSwaps={sortedUnclaimedSwaps}> - ); -}; - -const _UnclaimedSwaps = ({ unclaimedSwaps }: { unclaimedSwaps: UnclaimedSwapsWithMetadata[] }) => { - const { revalidate } = useRevalidator(); - const { claimSwap, isInProgress } = useStore(unclaimedSwapsSelector); - - return ( Unclaimed Swaps - {unclaimedSwaps.map(({ swap, asset1, asset2 }) => { + {unclaimedSwaps.data.map(({ swap, asset1, asset2 }) => { const id = uint8ArrayToBase64(getSwapRecordCommitment(swap).inner); return ( @@ -46,7 +39,7 @@ const _UnclaimedSwaps = ({ unclaimedSwaps }: { unclaimedSwaps: UnclaimedSwapsWit + *
+ * ) + * } + * ``` + */ +export function createZQuery< + State, + Name extends string, + DataType, + FetchArgs extends unknown[], + ProcessedDataType extends DataType = DataType, +>( + props: CreateZQueryUnaryProps, +): ZQuery; + +export function createZQuery< + State, + Name extends string, + DataType, + FetchArgs extends unknown[], + ProcessedDataType, +>( + props: CreateZQueryStreamingProps, +): ZQuery; + +export function createZQuery< + State, + Name extends string, + DataType, + FetchArgs extends unknown[], + ProcessedDataType = DataType, +>( + props: + | CreateZQueryUnaryProps + | CreateZQueryStreamingProps, +): ZQuery { + const { name, get, set, getUseStore } = props; + + const setAbortController = (abortController: AbortController | undefined) => { + set(prevState => ({ + ...prevState, + _zQueryInternal: { + ...prevState._zQueryInternal, + abortController, + }, + })); + }; + + const incrementReferenceCounter = () => { + const newReferenceCount = get(getUseStore().getState())._zQueryInternal.referenceCount + 1; + + set(prevState => ({ + ...prevState, + _zQueryInternal: { + ...prevState._zQueryInternal, + referenceCount: newReferenceCount, + }, + })); + + return newReferenceCount; + }; + + const decrementReferenceCounter = () => { + const newReferenceCount = get(getUseStore().getState())._zQueryInternal.referenceCount - 1; + + set(prevState => ({ + ...prevState, + _zQueryInternal: { + ...prevState._zQueryInternal, + referenceCount: newReferenceCount, + }, + })); + + return newReferenceCount; + }; + + return { + [`use${capitalize(name)}`]: (...args: FetchArgs) => { + const useStore = getUseStore(); + + useEffect(() => { + const fetch = get(useStore.getState())._zQueryInternal.fetch; + + { + const newReferenceCount = incrementReferenceCounter(); + + if (newReferenceCount === 1) { + setAbortController(new AbortController()); + void fetch(...args); + } + } + + const onUnmount = () => { + const newReferenceCount = decrementReferenceCounter(); + + if (newReferenceCount === 0) { + get(useStore.getState())._zQueryInternal.abortController?.abort(); + setAbortController(undefined); + } + }; + + return onUnmount; + }, [fetch]); + + const returnValue = useStore( + useShallow(state => { + const zQuery = get(state); + + return { + data: zQuery.data, + loading: zQuery.loading, + error: zQuery.error, + }; + }), + ); + + return returnValue; + }, + + [`useRevalidate${capitalize(name)}`]: () => { + const useStore = getUseStore(); + const returnValue = useStore(useShallow((state: State) => get(state).revalidate)); + return returnValue; + }, + + [name]: { + data: undefined, + loading: false, + error: undefined, + + revalidate: (...args: FetchArgs) => { + const { _zQueryInternal } = get(getUseStore().getState()); + _zQueryInternal.abortController?.abort(); + setAbortController(new AbortController()); + void _zQueryInternal.fetch(...args); + }, + + _zQueryInternal: { + referenceCount: 0, + + fetch: async (...args: FetchArgs) => { + const abortController = get(getUseStore().getState())._zQueryInternal.abortController; + // We have to use the `props` object (rather than its destructured + // properties) since we're passing the full `props` object to + // `isStreaming`, which is a type predicate. If we use previously + // destructured properties after the type predicate, the type + // predicate won't apply to them, since the type predicate was called + // after destructuring. + if (isStreaming(props)) { + const result = props.fetch(...args); + + props.set(prevState => ({ + ...prevState, + data: undefined, + })); + + try { + for await (const item of result) { + if (abortController?.signal.aborted) return; + + props.set(prevState => ({ + ...prevState, + data: props.stream(prevState.data, item), + })); + } + } catch (error) { + props.set(prevState => ({ ...prevState, error })); + } + } else { + try { + const data = await props.fetch(...args); + props.set(prevState => ({ ...prevState, data })); + } catch (error) { + props.set(prevState => ({ ...prevState, error })); + } + } + }, + }, + }, + } as ZQuery; +} diff --git a/packages/zquery/src/types.ts b/packages/zquery/src/types.ts new file mode 100644 index 00000000..c5120b1c --- /dev/null +++ b/packages/zquery/src/types.ts @@ -0,0 +1,190 @@ +export interface ZQueryState { + data?: DataType | undefined; + loading: boolean; + error?: unknown; + + revalidate: (...args: FetchArgs) => void; + + _zQueryInternal: { + referenceCount: number; + fetch: (...args: FetchArgs) => Promise; + abortController?: AbortController; + }; +} + +interface CreateZQueryCommonProps { + /** The name of this property in the state/slice. */ + name: Name; + /** + * A function that returns your `useStore` object -- e.g.: `() => useStore` + * + * If you passed `useStore` directly to `createZQuery`, which is called while + * defining `useStore`, you'd get a circular dependency. To work around that, + * pass a function that returns `useStore`, so that it can be used later (once + * `useStore` is defined). + */ + getUseStore: () => UseStore; +} + +export interface CreateZQueryUnaryProps< + Name extends string, + State, + DataType, + FetchArgs extends unknown[], +> extends CreateZQueryCommonProps { + stream?: undefined; + /** A function that executes the query. */ + fetch: (...args: FetchArgs) => Promise; + /** + * A selector that takes the root Zustand state and returns just this ZQuery + * state object. + * + * This ZQuery object doesn't know anything about the store or where this + * ZQuery object is located within it, so it can't call + * `getUseStore().getState()` and then navigate to its own location within + * state. Thus, you need to pass it a selector so it can find itself. + * + * ```ts + * createZQuery({ + * // ... + * // ... + * // ... + * // ... + * get: state => state.deeply.nested.object, + * }) + * ``` + */ + get: (state: State) => ZQueryState; + /** + * A setter that takes an updated ZQuery state object and assigns it to the + * location in your overall Zustand state object where this ZQuery state + * object is located. + * + * This ZQuery object doesn't know anything about the store or where this + * ZQuery object is located within it, so it can't call `set()` with the + * necessary spreads/etc. to ensure that the rest of the state is untouched + * when the ZQuery state is updated. So your setter needs to handle that. For + * example, if you have a deeply nested ZQuery object (located at + * `state.deeply.nested.object`) and you're using Zustand's Immer middleware + * to be able to imitate object mutations, you could pass this setter: + * + * ```ts + * createZQuery({ + * // ... + * // ... + * // ... + * set: newValue => { + * // `newValue` is the entire ZQuery state object, and can be assigned + * // as-is to the property that holds the ZQuery state. + * useStore.setState(state => { + * state.deeply.nested.object = newValue; + * }) + * }, + * // ... + * }) + * ``` + */ + set: (setter: >(prevState: T) => T) => void; +} + +export interface CreateZQueryStreamingProps< + Name extends string, + State, + DataType, + FetchArgs extends unknown[], + ProcessedDataType, +> extends CreateZQueryCommonProps { + /** A function that executes the query. */ + fetch: (...args: FetchArgs) => AsyncIterable; + /** + * Set to `true` if `fetch` will return a streaming response in the form of an + * `AsyncIterable`. + * + * Or, if you wish to modify the streaming results as they come in before + * they're added to the state, set this to a function that takes the current state + * as its first argument and the streamed item as its second, and returns the + * desired new state (or a promise containing the desired new state). This can + * be useful for e.g. sorting items in the state as new items are streamed. + */ + stream: (prevData: ProcessedDataType | undefined, item: DataType) => ProcessedDataType; + /** + * A selector that takes the root Zustand state and returns just this ZQuery + * state object. + * + * This ZQuery object doesn't know anything about the store or where this + * ZQuery object is located within it, so it can't call + * `getUseStore().getState()` and then navigate to its own location within + * state. Thus, you need to pass it a selector so it can find itself. + * + * ```ts + * createZQuery({ + * // ... + * // ... + * // ... + * // ... + * get: state => state.deeply.nested.object, + * }) + * ``` + */ + get: (state: State) => ZQueryState; + /** + * A setter that takes an updated ZQuery state object and assigns it to the + * location in your overall Zustand state object where this ZQuery state + * object is located. + * + * This ZQuery object doesn't know anything about the store or where this + * ZQuery object is located within it, so it can't call `set()` with the + * necessary spreads/etc. to ensure that the rest of the state is untouched + * when the ZQuery state is updated. So your setter needs to handle that. For + * example, if you have a deeply nested ZQuery object (located at + * `state.deeply.nested.object`) and you're using Zustand's Immer middleware + * to be able to imitate object mutations, you could pass this setter: + * + * ```ts + * createZQuery({ + * // ... + * // ... + * // ... + * set: newValue => { + * // `newValue` is the entire ZQuery state object, and can be assigned + * // as-is to the property that holds the ZQuery state. + * useStore.setState(state => { + * state.deeply.nested.object = newValue; + * }) + * }, + * // ... + * }) + * ``` + */ + set: ( + setter: >( + prevState: T, + ) => T, + ) => void; +} + +/** + * A simplified version of `UseBoundStore>`. + * + * We only use the `useStore()` hook and the `useStore.getState()` method in + * ZQuery, and we don't want to have to accommodate all of the different types + * that a `useStore` object can have when used with various middlewares, etc. + * For example, a "normal" `useStore` object is typed as + * `UseBoundStore>`, but a `useStore` object created with Immer + * middleware is typed as `UseBoundStore>>`. A + * function that accepts a `useStore` of the former type won't accept a + * `useStore` of the latter type (or of various other store types that mutated + * by middleware), so we'll just create a loose typing for `useStore` that can + * accommodate whatever middleware is being used. + */ +export type UseStore = ((selector: (state: State) => T) => T) & { getState(): State }; + +export type ZQuery = { + [key in `use${Capitalize}`]: (...args: FetchArgs) => { + data?: DataType; + loading: boolean; + error?: unknown; + }; +} & { + [key in `useRevalidate${Capitalize}`]: () => (...args: FetchArgs) => void; +} & Record>; diff --git a/packages/zquery/tsconfig.json b/packages/zquery/tsconfig.json new file mode 100644 index 00000000..2a9d5af4 --- /dev/null +++ b/packages/zquery/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/react-library.json", + "include": [".", "./tests-setup.ts"], + "exclude": ["dist", "build", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a4a55867..4d3dc56e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,7 +122,7 @@ importers: version: 3.3.0(vite@5.2.13(@types/node@20.14.2)(terser@5.31.1)) vitest: specifier: ^1.6.0 - version: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + version: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) apps/extension: dependencies: @@ -249,40 +249,40 @@ importers: version: 18.3.0 '@types/webpack': specifier: ^5.28.5 - version: 5.28.5(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4(webpack@5.92.0)) + version: 5.28.5(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) copy-webpack-plugin: specifier: ^12.0.2 - version: 12.0.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 12.0.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) css-loader: specifier: ^7.1.1 - version: 7.1.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 7.1.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) dotenv: specifier: ^16.4.5 version: 16.4.5 html-webpack-plugin: specifier: ^5.6.0 - version: 5.6.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 5.6.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) postcss: specifier: ^8.4.38 version: 8.4.38 postcss-loader: specifier: ^8.1.1 - version: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) promise.withresolvers: specifier: ^1.0.3 version: 1.0.3 style-loader: specifier: ^4.0.0 - version: 4.0.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 4.0.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) tailwindcss: specifier: ^3.4.3 version: 3.4.4(ts-node@10.9.2(@swc/core@1.5.28(@swc/helpers@0.5.11))(@types/node@20.14.2)(typescript@5.4.5)) ts-loader: specifier: ^9.5.1 - version: 9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + version: 9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) ts-node: specifier: ^10.9.2 version: 10.9.2(@swc/core@1.5.28(@swc/helpers@0.5.11))(@types/node@20.14.2)(typescript@5.4.5) @@ -294,7 +294,7 @@ importers: version: 3.3.0(vite@5.2.13(@types/node@20.14.2)(terser@5.31.1)) webpack: specifier: ^5.91.0 - version: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + version: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) webpack-cli: specifier: ^5.1.4 version: 5.1.4(webpack@5.92.0) @@ -358,6 +358,9 @@ importers: '@penumbra-zone/ui': specifier: workspace:* version: link:../../packages/ui + '@penumbra-zone/zquery': + specifier: workspace:* + version: link:../../packages/zquery '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) @@ -1118,6 +1121,22 @@ importers: specifier: ^6.0.0 version: 6.0.0 + packages/zquery: + dependencies: + react: + specifier: ^18.3.1 + version: 18.3.1 + zustand: + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.3.3)(immer@10.1.1)(react@18.3.1) + devDependencies: + '@types/react': + specifier: ^18.3.2 + version: 18.3.3 + vitest: + specifier: ^1.6.0 + version: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + packages: '@adobe/css-tools@4.4.0': @@ -18257,7 +18276,7 @@ snapshots: lodash: 4.17.21 redent: 3.0.0 optionalDependencies: - vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) '@testing-library/react@15.0.7(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: @@ -18586,11 +18605,11 @@ snapshots: '@types/uuid@9.0.8': {} - '@types/webpack@5.28.5(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4(webpack@5.92.0))': + '@types/webpack@5.28.5(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))': dependencies: '@types/node': 20.14.2 tapable: 2.2.1 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) transitivePeerDependencies: - '@swc/core' - esbuild @@ -18916,7 +18935,7 @@ snapshots: '@vitest/utils': 1.6.0 magic-string: 0.30.10 sirv: 2.0.4 - vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) optionalDependencies: playwright: 1.44.1 @@ -19340,19 +19359,19 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4))': + '@webpack-cli/configtest@2.1.1(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)))': dependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) webpack-cli: 5.1.4(webpack@5.92.0) - '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4))': + '@webpack-cli/info@2.0.2(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)))': dependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) webpack-cli: 5.1.4(webpack@5.92.0) - '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4))': + '@webpack-cli/serve@2.0.5(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)))': dependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) webpack-cli: 5.1.4(webpack@5.92.0) '@xtuc/ieee754@1.2.0': {} @@ -20356,7 +20375,7 @@ snapshots: dependencies: toggle-selection: 1.0.6 - copy-webpack-plugin@12.0.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + copy-webpack-plugin@12.0.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: fast-glob: 3.3.2 glob-parent: 6.0.2 @@ -20364,7 +20383,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.2.0 serialize-javascript: 6.0.2 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) core-js-compat@3.37.1: dependencies: @@ -20556,7 +20575,7 @@ snapshots: semver: 6.3.1 webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2) - css-loader@7.1.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + css-loader@7.1.2(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: icss-utils: 5.1.0(postcss@8.4.38) postcss: 8.4.38 @@ -20567,7 +20586,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.6.2 optionalDependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) css-select@4.3.0: dependencies: @@ -21320,7 +21339,7 @@ snapshots: '@typescript-eslint/utils': 7.13.0(eslint@9.4.0)(typescript@5.4.5) eslint: 9.4.0 optionalDependencies: - vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) + vitest: 1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1) transitivePeerDependencies: - supports-color - typescript @@ -22255,7 +22274,7 @@ snapshots: html-tags@3.3.1: {} - html-webpack-plugin@5.6.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + html-webpack-plugin@5.6.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -22263,7 +22282,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) htmlparser2@6.1.0: dependencies: @@ -24056,14 +24075,14 @@ snapshots: semver: 7.6.2 webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2) - postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + postcss-loader@8.1.1(postcss@8.4.38)(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: cosmiconfig: 9.0.0(typescript@5.4.5) jiti: 1.21.6 postcss: 8.4.38 semver: 7.6.2 optionalDependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) transitivePeerDependencies: - typescript @@ -25348,9 +25367,9 @@ snapshots: schema-utils: 2.7.1 webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2) - style-loader@4.0.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + style-loader@4.0.0(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) styled-components@6.1.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: @@ -25573,28 +25592,29 @@ snapshots: term-size@2.2.1: {} - terser-webpack-plugin@5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)): + terser-webpack-plugin@5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.1 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) optionalDependencies: '@swc/core': 1.5.28(@swc/helpers@0.5.11) esbuild: 0.20.2 - terser-webpack-plugin@5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + terser-webpack-plugin@5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 3.3.0 serialize-javascript: 6.0.2 terser: 5.31.1 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2) optionalDependencies: '@swc/core': 1.5.28(@swc/helpers@0.5.11) + esbuild: 0.20.2 terser@5.31.1: dependencies: @@ -25709,7 +25729,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-loader@9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)): + ts-loader@9.5.1(typescript@5.4.5)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))): dependencies: chalk: 4.1.2 enhanced-resolve: 5.17.0 @@ -25717,7 +25737,7 @@ snapshots: semver: 7.6.2 source-map: 0.7.4 typescript: 5.4.5 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) ts-node@10.9.2(@swc/core@1.5.28(@swc/helpers@0.5.11))(@types/node@20.14.2)(typescript@5.4.5): dependencies: @@ -26203,7 +26223,7 @@ snapshots: fsevents: 2.3.3 terser: 5.31.1 - vitest@1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1): + vitest@1.6.0(@types/node@20.14.2)(@vitest/browser@1.6.0(playwright@1.44.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))(terser@5.31.1): dependencies: '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 @@ -26276,9 +26296,9 @@ snapshots: webpack-cli@5.1.4(webpack@5.92.0): dependencies: '@discoveryjs/json-ext': 0.5.7 - '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) - '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) - '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + '@webpack-cli/configtest': 2.1.1(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) + '@webpack-cli/info': 2.0.2(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) + '@webpack-cli/serve': 2.0.5(webpack-cli@5.1.4(webpack@5.92.0))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) colorette: 2.0.20 commander: 10.0.1 cross-spawn: 7.0.3 @@ -26287,7 +26307,7 @@ snapshots: import-local: 3.1.0 interpret: 3.1.1 rechoir: 0.8.0 - webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4) + webpack: 5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)) webpack-merge: 5.10.0 webpack-merge@5.10.0: @@ -26331,7 +26351,7 @@ snapshots: - esbuild - uglify-js - webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4): + webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.5 @@ -26354,7 +26374,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(webpack-cli@5.1.4)) + terser-webpack-plugin: 5.3.10(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack@5.92.0(@swc/core@1.5.28(@swc/helpers@0.5.11))(esbuild@0.20.2)(webpack-cli@5.1.4(webpack@5.92.0))) watchpack: 2.4.1 webpack-sources: 3.2.3 optionalDependencies: