diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 9e2d9bae2..24092f509 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -19,7 +19,6 @@ body: - "@suspensive/react-query-next-experimental" - "@suspensive/react-query-next-experimental-4" - "@suspensive/jotai" - - "@suspensive/cache" - "@suspensive/react-image" - etc validations: diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index c93cc78d0..2412c08ef 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -19,7 +19,6 @@ body: - "@suspensive/react-query-next-experimental" - "@suspensive/react-query-next-experimental-4" - "@suspensive/jotai" - - "@suspensive/cache" - "@suspensive/react-image" - etc validations: diff --git a/.github/labeler.yml b/.github/labeler.yml index 7689cfd82..cc46d8266 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -18,8 +18,6 @@ - "packages/react-query-next-experimental-4/**/*" "@suspensive/jotai": - "packages/jotai/**/*" -"@suspensive/cache": - - "packages/cache/**/*" "@suspensive/react-image": - "packages/react-image/**/*" "suspensive.org": diff --git a/README.md b/README.md index d6af5e4ef..376c71dee 100644 --- a/README.md +++ b/README.md @@ -34,12 +34,6 @@ Key features: useSuspenseQuery, useSuspenseQueries, useSuspenseInfiniteQuery, qu Key features: Atom, AtomValue, SetAtom, and more. -### @suspensive/cache [![npm version](https://img.shields.io/npm/v/@suspensive/cache?color=000&labelColor=000&logo=npm&label=)](https://www.npmjs.com/package/@suspensive/cache) [![npm](https://img.shields.io/npm/dm/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) [![npm bundle size](https://img.shields.io/bundlephobia/minzip/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) - -> This package provides caching solutions that can be used within React applications. It includes hooks like useRead and useCache, as well as providers for managing and storing cache data efficiently. - -Key features: Read, useRead, Cache, useCache, CacheProvider, and more. -
## Visit [suspensive.org](https://suspensive.org) for docs, guides, API and more! diff --git a/codecov.yml b/codecov.yml index 59de62600..621c00078 100644 --- a/codecov.yml +++ b/codecov.yml @@ -60,10 +60,6 @@ component_management: name: '@suspensive/jotai' paths: - packages/jotai/** - - component_id: cache - name: '@suspensive/cache' - paths: - - packages/cache/** - component_id: react-image name: '@suspensive/react-image' paths: diff --git a/docs/suspensive.org/src/pages/docs/changelogs.en.mdx b/docs/suspensive.org/src/pages/docs/changelogs.en.mdx index ce2530055..2faa7367b 100644 --- a/docs/suspensive.org/src/pages/docs/changelogs.en.mdx +++ b/docs/suspensive.org/src/pages/docs/changelogs.en.mdx @@ -13,5 +13,4 @@ import { Cards, Card } from 'nextra/components' href="https://github.com/toss/suspensive/blob/main/packages/react-image/CHANGELOG.md" /> - diff --git a/docs/suspensive.org/src/pages/docs/changelogs.ko.mdx b/docs/suspensive.org/src/pages/docs/changelogs.ko.mdx index ce2530055..2faa7367b 100644 --- a/docs/suspensive.org/src/pages/docs/changelogs.ko.mdx +++ b/docs/suspensive.org/src/pages/docs/changelogs.ko.mdx @@ -13,5 +13,4 @@ import { Cards, Card } from 'nextra/components' href="https://github.com/toss/suspensive/blob/main/packages/react-image/CHANGELOG.md" /> - diff --git a/examples/visualization/package.json b/examples/visualization/package.json index 7f1f94f80..baeb13a82 100644 --- a/examples/visualization/package.json +++ b/examples/visualization/package.json @@ -12,7 +12,6 @@ "start": "next start -p 4001" }, "dependencies": { - "@suspensive/cache": "workspace:*", "@suspensive/react": "workspace:*", "@suspensive/react-dom": "workspace:*", "@suspensive/react-image": "workspace:*", diff --git a/examples/visualization/src/app/cache/SuspenseCache/page.tsx b/examples/visualization/src/app/cache/SuspenseCache/page.tsx deleted file mode 100644 index 6098d4da2..000000000 --- a/examples/visualization/src/app/cache/SuspenseCache/page.tsx +++ /dev/null @@ -1,38 +0,0 @@ -'use client' - -import { Read, cacheOptions, useCache } from '@suspensive/cache' -import { ErrorBoundary, Suspense } from '@suspensive/react' -import { api } from '~/utils' - -const successCache = (ms: number) => - cacheOptions({ - cacheKey: [ms] as const, - cacheFn: () => api.delay(ms, { percentage: 100 }), - }) - -export default function Page() { - const cache = useCache() - - return ( -
error
}> -
- loading...
}> - - {(cached) => ( -
- -
{cached.data}
-
- )} -
- - -
- ) -} diff --git a/examples/visualization/src/app/providers.tsx b/examples/visualization/src/app/providers.tsx index d55ec0459..29ce27f20 100644 --- a/examples/visualization/src/app/providers.tsx +++ b/examples/visualization/src/app/providers.tsx @@ -1,6 +1,5 @@ 'use client' -import { Cache, CacheProvider } from '@suspensive/cache' import { DefaultProps, DefaultPropsProvider } from '@suspensive/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { ReactQueryDevtools } from '@tanstack/react-query-devtools' @@ -8,18 +7,15 @@ import { type PropsWithChildren, useState } from 'react' import { Spinner } from '~/components/uis' export const Providers = ({ children }: PropsWithChildren) => { - const cache = useState(() => new Cache())[0] const queryClient = useState(() => new QueryClient({ defaultOptions: { queries: { retry: 0 } } }))[0] const defaultProps = useState(() => new DefaultProps({ Delay: { ms: 1200 }, Suspense: { fallback: } }))[0] return ( - - - - {children} - - - - + + + {children} + + + ) } diff --git a/packages/cache/CHANGELOG.md b/packages/cache/CHANGELOG.md deleted file mode 100644 index 3c7b9ae6c..000000000 --- a/packages/cache/CHANGELOG.md +++ /dev/null @@ -1,195 +0,0 @@ -# @suspensive/cache - -## 0.5.7 - -### Patch Changes - -- [#1272](https://github.com/toss/suspensive/pull/1272) [`89f5b5c`](https://github.com/toss/suspensive/commit/89f5b5c4d9b16bcbed77ef3e17bb1f34babe2921) Thanks [@love1ace](https://github.com/love1ace)! - docs(\*): update description of package.json - -- Updated dependencies [[`89f5b5c`](https://github.com/toss/suspensive/commit/89f5b5c4d9b16bcbed77ef3e17bb1f34babe2921)]: - - @suspensive/utils@2.17.1 - -## 0.5.6 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.17.0 - -## 0.5.5 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.16.1 - -## 0.5.4 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.16.0 - -## 0.5.3 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.15.0 - -## 0.5.2 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.14.2 - -## 0.5.1 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.14.1 - -## 0.5.0 - -### Minor Changes - -- [#1224](https://github.com/toss/suspensive/pull/1224) [`c077fc5`](https://github.com/toss/suspensive/commit/c077fc53ec1e380e238cc722eb1865b1812a2db8) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - fix(cache): rename the cache interface name - -## 0.4.1 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.14.0 - -## 0.4.0 - -### Minor Changes - -- [#1197](https://github.com/toss/suspensive/pull/1197) [`0bb493c`](https://github.com/toss/suspensive/commit/0bb493cd34300dc747abd6c138895de8d720160b) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - feat(cache): add `Subscribable` - -## 0.3.8 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.13.1 - -## 0.3.7 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.13.0 - -## 0.3.6 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.12.3 - -## 0.3.5 - -### Patch Changes - -- [#1189](https://github.com/toss/suspensive/pull/1189) [`af75cbf`](https://github.com/toss/suspensive/commit/af75cbf0d6f848a1f07099731d50d3904df2facc) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - fix(cache): remove unsubscribe method of CacheStore - -## 0.3.4 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.12.2 - -## 0.3.3 - -### Patch Changes - -- [#1180](https://github.com/toss/suspensive/pull/1180) [`f79b96c`](https://github.com/toss/suspensive/commit/f79b96c728e15ebe819445cf5d1c2e33e6c96ef4) Thanks [@manudeli](https://github.com/manudeli)! - fix(\*): remove unnecessary devDeps(react-dom, @types/react-dom) - -- Updated dependencies []: - - @suspensive/utils@2.12.1 - -## 0.3.2 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.12.0 - -## 0.3.1 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.11.0 - -## 0.3.0 - -### Minor Changes - -- [#1140](https://github.com/toss/suspensive/pull/1140) [`eb5d257`](https://github.com/toss/suspensive/commit/eb5d25767013ce1125b17948028c48fb15507037) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - feat(cache): add `remove` method to CacheStore - -## 0.2.7 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.10.0 - -## 0.2.6 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.9.4 - -## 0.2.5 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.9.3 - -## 0.2.4 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.9.2 - -## 0.2.3 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.9.1 - -## 0.2.2 - -### Patch Changes - -- Updated dependencies []: - - @suspensive/utils@2.9.0 - -## 0.2.1 - -### Patch Changes - -- Updated dependencies [[`2326b13`](https://github.com/toss/suspensive/commit/2326b1341f167454a889953fb0bbf58449e1ca98)]: - - @suspensive/utils@2.8.1 - -## 0.2.0 - -### Minor Changes - -- [#1089](https://github.com/toss/suspensive/pull/1089) [`be21828`](https://github.com/toss/suspensive/commit/be218284dc67ccc84ffc29a0bfd84c578c1f37f1) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - feat(cache): add DataTag feature of cacheOptions - -## 0.1.0 - -### Minor Changes - -- [#1083](https://github.com/toss/suspensive/pull/1083) [`a7c5c58`](https://github.com/toss/suspensive/commit/a7c5c58521ab1aac93420c0a896683917c741af5) Thanks [@SEOKKAMONI](https://github.com/SEOKKAMONI)! - feat(cache): add CacheStore, useCacheStore, CacheStoreProvider, useCache, Cache, cacheOptions diff --git a/packages/cache/LICENSE b/packages/cache/LICENSE deleted file mode 100644 index 69dc6a701..000000000 --- a/packages/cache/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024 Viva Republica, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/cache/README.ko.md b/packages/cache/README.ko.md deleted file mode 100644 index 963bb1839..000000000 --- a/packages/cache/README.ko.md +++ /dev/null @@ -1,13 +0,0 @@ -# 라이브러리 소개 - -[![npm version](https://img.shields.io/npm/v/@suspensive/cache?color=000&labelColor=000&logo=npm&label=)](https://www.npmjs.com/package/@suspensive/cache) -[![npm](https://img.shields.io/npm/dm/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) -[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) - -## 설치하기 - -@suspensive/cache 는 npm에 있습니다. 최신 안정버전을 설치하기 위해 아래 커맨드를 실행하세요 - -```shell npm2yarn -npm install @suspensive/cache -``` diff --git a/packages/cache/README.md b/packages/cache/README.md deleted file mode 100644 index 9c3ebc2d9..000000000 --- a/packages/cache/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Introduction - -[![npm version](https://img.shields.io/npm/v/@suspensive/cache?color=000&labelColor=000&logo=npm&label=)](https://www.npmjs.com/package/@suspensive/cache) -[![npm](https://img.shields.io/npm/dm/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) -[![npm bundle size](https://img.shields.io/bundlephobia/minzip/@suspensive/cache?color=000&labelColor=000)](https://www.npmjs.com/package/@suspensive/cache) - -## Installation - -@suspensive/cache lives in npm. To install the latest stable version, run the following command - -```shell npm2yarn -npm install @suspensive/cache -``` diff --git a/packages/cache/eslint.config.mjs b/packages/cache/eslint.config.mjs deleted file mode 100644 index 0daa8aa5b..000000000 --- a/packages/cache/eslint.config.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path' -import { fileURLToPath } from 'url' -import { suspensiveReactTypeScriptConfig } from '@suspensive/eslint-config' - -export default [ - ...suspensiveReactTypeScriptConfig, - { - languageOptions: { - parserOptions: { - tsconfigRootDir: path.dirname(fileURLToPath(import.meta.url)), - project: './tsconfig.json', - }, - }, - }, -] diff --git a/packages/cache/package.json b/packages/cache/package.json deleted file mode 100644 index 8b11454a4..000000000 --- a/packages/cache/package.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "name": "@suspensive/cache", - "version": "0.5.7", - "description": "Suspensive interfaces for caching", - "keywords": [ - "suspensive", - "react", - "cache" - ], - "homepage": "https://suspensive.org", - "bugs": "https://github.com/toss/suspensive/issues", - "repository": { - "type": "git", - "url": "git+https://github.com/toss/suspensive.git", - "directory": "packages/cache" - }, - "license": "MIT", - "author": "Jonghyeon Ko & Seokjin Kim ", - "sideEffects": false, - "type": "module", - "exports": { - ".": { - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - } - }, - "./package.json": "./package.json" - }, - "main": "dist/index.cjs", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "files": [ - "dist", - "src" - ], - "scripts": { - "build": "tsup", - "ci:attw": "attw --pack", - "ci:eslint": "eslint \"**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}\"", - "ci:publint": "publint --strict", - "ci:test": "vitest run --coverage --typecheck", - "ci:type": "tsc --noEmit", - "clean": "rimraf ./dist && rimraf ./coverage", - "prepack": "pnpm build", - "test:ui": "vitest --ui --coverage --typecheck" - }, - "dependencies": { - "@suspensive/utils": "workspace:*" - }, - "devDependencies": { - "@suspensive/eslint-config": "workspace:*", - "@suspensive/react": "workspace:*", - "@suspensive/tsconfig": "workspace:*", - "@suspensive/tsup": "workspace:*", - "@types/react": "catalog:react18", - "react": "catalog:react18" - }, - "peerDependencies": { - "react": "^18" - }, - "publishConfig": { - "access": "public" - } -} diff --git a/packages/cache/src/Cache.ts b/packages/cache/src/Cache.ts deleted file mode 100644 index 874281d38..000000000 --- a/packages/cache/src/Cache.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { Subscribable } from './models/Subscribable' -import type { CacheKey, CacheOptions } from './types' -import type { ExtractPartial } from './utility-types/ExtractPartial' -import { hashCacheKey } from './utils' - -const enum CacheStatus { - Idle = 'idle', - Pending = 'pending', - Resolved = 'resolved', - Rejected = 'rejected', -} - -export type AllStatusCached = - | { - status: CacheStatus.Idle - promiseToSuspend: undefined - cacheKey: TCacheKey - hashedCacheKey: ReturnType - state: { - promise: undefined - data: undefined - error: undefined - } - } - | { - status: CacheStatus.Pending - promiseToSuspend: Promise - cacheKey: TCacheKey - hashedCacheKey: ReturnType - state: { - promise: Promise - data: undefined - error: undefined - } - } - | { - status: CacheStatus.Resolved - promiseToSuspend: Promise - cacheKey: TCacheKey - hashedCacheKey: ReturnType - state: { - promise: Promise - data: TData - error: undefined - } - } - | { - status: CacheStatus.Rejected - promiseToSuspend: Promise - cacheKey: TCacheKey - hashedCacheKey: ReturnType - state: { - promise: Promise - data: undefined - error: unknown - } - } - -export type IdleCached = ExtractPartial< - AllStatusCached, - { status: CacheStatus.Idle } -> -export type PendingCached = ExtractPartial< - AllStatusCached, - { status: CacheStatus.Pending } -> -export type ResolvedCached = ExtractPartial< - AllStatusCached, - { status: CacheStatus.Resolved } -> -export type RejectedCached = ExtractPartial< - AllStatusCached, - { status: CacheStatus.Rejected } -> - -export type Cached = - | IdleCached - | PendingCached - | ResolvedCached - | RejectedCached - -/** - * @experimental This is experimental feature. - */ -export class Cache extends Subscribable<() => void> { - private cache = new Map, Cached>() - - public reset = (options?: Pick, 'cacheKey'>) => { - if (typeof options?.cacheKey === 'undefined' || options.cacheKey.length === 0) { - this.cache.clear() - this.notify() - return - } - - const hashedCacheKey = hashCacheKey(options.cacheKey) - - if (this.cache.has(hashedCacheKey)) { - this.cache.delete(hashedCacheKey) - } - - this.notify() - } - - public remove = (options: Pick, 'cacheKey'>) => { - const hashedCacheKey = hashCacheKey(options.cacheKey) - - if (this.cache.has(hashedCacheKey)) { - this.cache.delete(hashedCacheKey) - } - } - - public clearError = (options?: Pick, 'cacheKey'>) => { - if (options?.cacheKey === undefined || options.cacheKey.length === 0) { - this.cache.forEach((cached, hashedCacheKey, cache) => { - cache.set(hashedCacheKey, { - ...cached, - status: CacheStatus.Idle, - promiseToSuspend: undefined, - state: { - promise: undefined, - error: undefined, - data: undefined, - }, - }) - }) - return - } - - const hashedCacheKey = hashCacheKey(options.cacheKey) - const cached = this.cache.get(hashedCacheKey) - if (cached) { - this.cache.set(hashedCacheKey, { - ...cached, - status: CacheStatus.Idle, - promiseToSuspend: undefined, - state: { - promise: undefined, - error: undefined, - data: undefined, - }, - }) - } - } - - public suspend = ({ - cacheKey, - cacheFn, - }: CacheOptions): ResolvedCached => { - const hashedCacheKey = hashCacheKey(cacheKey) - const cached = this.cache.get(hashedCacheKey) - if (cached && cached.status !== CacheStatus.Idle) { - if (cached.status === CacheStatus.Rejected) { - throw cached.state.error - } - if (cached.status === CacheStatus.Resolved) { - return cached as ResolvedCached - } - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw cached.promiseToSuspend - } - - const promise = cacheFn({ cacheKey }) - const newCached: Cached = { - cacheKey, - hashedCacheKey, - status: CacheStatus.Pending, - state: { - promise, - data: undefined, - error: undefined, - }, - promiseToSuspend: promise.then( - (data) => { - newCached.status = CacheStatus.Resolved - newCached.state.data = data - newCached.state.error = undefined - }, - (error: unknown) => { - newCached.status = CacheStatus.Rejected - newCached.state.data = undefined - newCached.state.error = error - } - ), - } - - this.cache.set(hashedCacheKey, newCached) - // eslint-disable-next-line @typescript-eslint/only-throw-error - throw newCached.promiseToSuspend - } - - public getData = (options: Pick, 'cacheKey'>) => - this.cache.get(hashCacheKey(options.cacheKey))?.state.data - public getError = (options: Pick, 'cacheKey'>) => - this.cache.get(hashCacheKey(options.cacheKey))?.state.error -} diff --git a/packages/cache/src/CacheProvider.tsx b/packages/cache/src/CacheProvider.tsx deleted file mode 100644 index b71601d8e..000000000 --- a/packages/cache/src/CacheProvider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { type PropsWithChildren } from 'react' -import type { Cache } from './Cache' -import { CacheContext } from './contexts' - -type CacheProviderProps = PropsWithChildren<{ cache: Cache }> - -/** - * @experimental This is experimental feature. - */ -export function CacheProvider({ cache, children }: CacheProviderProps) { - return {children} -} diff --git a/packages/cache/src/CacheStore.spec.tsx b/packages/cache/src/CacheStore.spec.tsx deleted file mode 100644 index 3409a1d62..000000000 --- a/packages/cache/src/CacheStore.spec.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/utils' -import { render, screen, waitFor } from '@testing-library/react' -import ms from 'ms' -import { Suspense } from 'react' -import { Cache } from './Cache' -import { cacheOptions } from './cacheOptions' -import { CacheProvider } from './CacheProvider' -import { Read } from './Read' - -const errorCache = (id: number) => - cacheOptions({ - cacheKey: ['key', id] as const, - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - cacheFn: () => sleep(ms('0.1s')).then(() => Promise.reject(ERROR_MESSAGE)), - }) - -const successCache = (id: number) => - cacheOptions({ - cacheKey: ['key', id] as const, - cacheFn: () => sleep(ms('0.1s')).then(() => Promise.resolve(TEXT)), - }) - -describe('Cache', () => { - let cache: Cache - - beforeEach(() => { - cache = new Cache() - cache.reset() - }) - - describe('clearError', () => { - it('should clear promise & error for all cached', async () => { - expect(cache.getError(errorCache(1))).toBeUndefined() - expect(cache.getError(errorCache(2))).toBeUndefined() - try { - cache.suspend(errorCache(1)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - try { - cache.suspend(errorCache(2)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - expect(cache.getError(errorCache(1))).toBe(ERROR_MESSAGE) - expect(cache.getError(errorCache(2))).toBe(ERROR_MESSAGE) - - cache.clearError() - expect(cache.getError(errorCache(1))).toBeUndefined() - expect(cache.getError(errorCache(2))).toBeUndefined() - }) - - it('should clear promise & error for specific cached', async () => { - expect(cache.getError(errorCache(1))).toBeUndefined() - expect(cache.getError(errorCache(2))).toBeUndefined() - try { - cache.suspend(errorCache(1)) - } catch (promiseToSuspense) { - expect(await promiseToSuspense).toBeUndefined() - } - try { - cache.suspend(errorCache(1)) - } catch (error) { - expect(error).toBe(ERROR_MESSAGE) - } - try { - cache.suspend(errorCache(2)) - } catch (promiseToSuspense) { - expect(await promiseToSuspense).toBeUndefined() - } - try { - cache.suspend(errorCache(2)) - } catch (error) { - expect(error).toBe(ERROR_MESSAGE) - } - expect(cache.getError(errorCache(1))).toBe(ERROR_MESSAGE) - expect(cache.getError(errorCache(2))).toBe(ERROR_MESSAGE) - - cache.clearError(errorCache(1)) - expect(cache.getError(errorCache(1))).toBeUndefined() - expect(cache.getError(errorCache(2))).toBe(ERROR_MESSAGE) - cache.clearError(errorCache(2)) - expect(cache.getError(errorCache(1))).toBeUndefined() - expect(cache.getError(errorCache(2))).toBeUndefined() - }) - }) - - describe('remove', () => { - it('should remove specific cached', async () => { - try { - cache.suspend(successCache(1)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - expect(cache.getData(successCache(1))).toBe(TEXT) - expect(() => { - cache.remove(successCache(1)) - }).not.throw() - expect(cache.getData(successCache(1))).toBeUndefined() - }) - }) - - describe('reset', () => { - it('should delete all cached and notify to subscribers', async () => { - const mockListener1 = vitest.fn() - const mockListener2 = vitest.fn() - cache.subscribe(mockListener1) - cache.subscribe(mockListener2) - try { - cache.suspend(successCache(1)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - try { - cache.suspend(successCache(2)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - expect(cache.getData(successCache(1))).toBe(TEXT) - expect(cache.getData(successCache(2))).toBe(TEXT) - expect(mockListener1).not.toHaveBeenCalled() - expect(mockListener2).not.toHaveBeenCalled() - cache.reset() - expect(cache.getData(successCache(1))).toBeUndefined() - expect(cache.getData(successCache(2))).toBeUndefined() - expect(mockListener1).toHaveBeenCalledOnce() - expect(mockListener2).toHaveBeenCalledOnce() - }) - - it('should delete specific cached and notify to subscriber', async () => { - const mockListener = vitest.fn() - cache.subscribe(mockListener) - try { - cache.suspend(successCache(1)) - } catch (promiseToSuspense) { - await promiseToSuspense - } - expect(cache.getData(successCache(1))).toBe(TEXT) - expect(mockListener).not.toHaveBeenCalled() - cache.reset(successCache(1)) - expect(cache.getData(successCache(1))).toBeUndefined() - expect(mockListener).toHaveBeenCalledOnce() - }) - }) - - describe('getData', () => { - it('should get data of specific cached', async () => { - render( - - - sleep(ms('0.1s')).then(() => TEXT)}> - {(cached) => <>{cached.data}} - - - - ) - - expect(screen.queryByText(FALLBACK)).toBeInTheDocument() - expect(screen.queryByText(TEXT)).not.toBeInTheDocument() - expect(cache.getData(errorCache(1))).toBeUndefined() - await waitFor(() => expect(screen.queryByText(TEXT)).toBeInTheDocument()) - expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() - expect(cache.getData(errorCache(1))).toBe(TEXT) - }) - }) -}) diff --git a/packages/cache/src/Read.spec.tsx b/packages/cache/src/Read.spec.tsx deleted file mode 100644 index f81f8256a..000000000 --- a/packages/cache/src/Read.spec.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Suspense } from '@suspensive/react' -import { TEXT } from '@suspensive/utils' -import { render, screen } from '@testing-library/react' -import { Cache } from './Cache' -import { cacheOptions } from './cacheOptions' -import { CacheProvider } from './CacheProvider' -import { Read } from './Read' - -const successCache = (id: number) => - cacheOptions({ - cacheKey: ['key', id] as const, - cacheFn: () => Promise.resolve(TEXT), - }) - -describe('', () => { - let cache: Cache - - beforeEach(() => { - cache = new Cache() - }) - - it('should render child component with data from useRead hook', async () => { - render( - - - {(cached) => <>{cached.data}} - - - ) - - expect(await screen.findByText(TEXT)).toBeInTheDocument() - }) -}) diff --git a/packages/cache/src/Read.tsx b/packages/cache/src/Read.tsx deleted file mode 100644 index 783a63150..000000000 --- a/packages/cache/src/Read.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import type { ResolvedCached } from './Cache' -import type { CacheKey, CacheOptions } from './types' -import { useRead } from './useRead' - -/** - * @experimental This is experimental feature. - */ -export interface ReadProps extends CacheOptions { - children: (props: ResolvedCached['state']) => JSX.Element -} - -/** - * @experimental This is experimental feature. - */ -export function Read({ children, ...options }: ReadProps) { - return <>{children(useRead(options))} -} diff --git a/packages/cache/src/cacheOptions.spec.tsx b/packages/cache/src/cacheOptions.spec.tsx deleted file mode 100644 index b47f93f57..000000000 --- a/packages/cache/src/cacheOptions.spec.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { FALLBACK, TEXT } from '@suspensive/utils' -import { render, screen } from '@testing-library/react' -import { Suspense } from 'react' -import { Cache } from './Cache' -import { cacheOptions } from './cacheOptions' -import { CacheProvider } from './CacheProvider' -import { Read } from './Read' -import { useRead } from './useRead' - -const key = (id: number) => ['key', id] as const - -const successCache = () => cacheOptions({ cacheKey: key(1), cacheFn: () => Promise.resolve(TEXT) }) - -describe('cacheOptions', () => { - let cache: Cache - - beforeEach(() => { - cache = new Cache() - }) - - it('should be used with Read', async () => { - render( - - - {(cached) => <>{cached.data}} - - - ) - - expect(await screen.findByText(TEXT)).toBeInTheDocument() - }) - - it('should be used with useRead', async () => { - const CacheComponent = () => { - const cached = useRead(successCache()) - return <>{cached.data} - } - - render( - - - - - - ) - - expect(await screen.findByText(TEXT)).toBeInTheDocument() - }) -}) diff --git a/packages/cache/src/cacheOptions.test-d.tsx b/packages/cache/src/cacheOptions.test-d.tsx deleted file mode 100644 index e5eb218a1..000000000 --- a/packages/cache/src/cacheOptions.test-d.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { expectTypeOf } from 'vitest' -import type { ResolvedCached } from './Cache' -import { cacheOptions } from './cacheOptions' -import { Read } from './Read' -import { dataTagSymbol } from './types' -import { useRead } from './useRead' - -const key = (id: number) => ['key', id] as const - -const successCache = () => - cacheOptions({ - cacheKey: key(1), - cacheFn: () => Promise.resolve(5), - }) - -describe('cacheOptions', () => { - it('should be used with ', () => { - ;(() => ( - - {(cached) => { - expectTypeOf(cached).toEqualTypeOf['state']>() - expectTypeOf(cached.data).toEqualTypeOf() - return <> - }} - - ))() - }) - - it('should be used with useRead', () => { - const cached = useRead(successCache()) - expectTypeOf(cached).toEqualTypeOf['state']>() - expectTypeOf(cached.data).toEqualTypeOf() - }) - - it('should add DataTag on cacheKey with ReturnType', () => { - expectTypeOf(successCache().cacheKey[dataTagSymbol]).toEqualTypeOf() - }) -}) diff --git a/packages/cache/src/cacheOptions.ts b/packages/cache/src/cacheOptions.ts deleted file mode 100644 index bd57e93fc..000000000 --- a/packages/cache/src/cacheOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { CacheKey, CacheOptions, DataTag } from './types' - -/** - * @experimental This is experimental feature. - */ -export function cacheOptions( - options: CacheOptions -): CacheOptions & { - cacheKey: DataTag -} - -/** - * @experimental This is experimental feature. - */ -export function cacheOptions(options: unknown) { - return options -} diff --git a/packages/cache/src/contexts/CacheContext.ts b/packages/cache/src/contexts/CacheContext.ts deleted file mode 100644 index 07e927c35..000000000 --- a/packages/cache/src/contexts/CacheContext.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react' -import type { Cache } from '../Cache' - -export const CacheContext = createContext(null) diff --git a/packages/cache/src/contexts/index.ts b/packages/cache/src/contexts/index.ts deleted file mode 100644 index 584cd5ae9..000000000 --- a/packages/cache/src/contexts/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CacheContext } from './CacheContext' diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts deleted file mode 100644 index cea614db3..000000000 --- a/packages/cache/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { Read } from './Read' -export { useRead } from './useRead' -export { cacheOptions } from './cacheOptions' -export { Cache } from './Cache' -export { CacheProvider } from './CacheProvider' -export { useCache } from './useCache' diff --git a/packages/cache/src/models/Subscribable.ts b/packages/cache/src/models/Subscribable.ts deleted file mode 100644 index aeeed145b..000000000 --- a/packages/cache/src/models/Subscribable.ts +++ /dev/null @@ -1,19 +0,0 @@ -type Listener = (...args: any[]) => unknown - -type Unsubscribe = () => void -type Subscribe = (listener: TListener) => Unsubscribe - -export class Subscribable { - protected listeners = new Set() - - public subscribe: Subscribe = (listener) => { - this.listeners.add(listener) - - const unsubscribe = (): void => { - this.listeners.delete(listener) - } - return unsubscribe - } - - protected notify = (): void => this.listeners.forEach((listener) => listener()) -} diff --git a/packages/cache/src/types.ts b/packages/cache/src/types.ts deleted file mode 100644 index 088b2bcf4..000000000 --- a/packages/cache/src/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Tuple } from './utility-types' - -export type CacheKey = Tuple - -/** - * @experimental This is experimental feature. - */ -export interface CacheOptions { - cacheKey: TCacheKey - cacheFn: (options: { cacheKey: TCacheKey }) => Promise -} - -export declare const dataTagSymbol: unique symbol -export type DataTag = TType & { - [dataTagSymbol]: TValue -} diff --git a/packages/cache/src/useCache.ts b/packages/cache/src/useCache.ts deleted file mode 100644 index 44c78fcb9..000000000 --- a/packages/cache/src/useCache.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { useContext } from 'react' -import type { Cache } from './Cache' -import { CacheContext } from './contexts' - -/** - * @experimental This is experimental feature. - */ -export function useCache(): Cache { - const cache = useContext(CacheContext) - if (cache == null) { - throw new Error('CacheProvider should be in parent') - } - return cache -} diff --git a/packages/cache/src/useRead.spec.tsx b/packages/cache/src/useRead.spec.tsx deleted file mode 100644 index 9f7a1a44b..000000000 --- a/packages/cache/src/useRead.spec.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ErrorBoundary, Suspense } from '@suspensive/react' -import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/utils' -import { render, screen, waitFor } from '@testing-library/react' -import ms from 'ms' -import { Cache } from './Cache' -import { cacheOptions } from './cacheOptions' -import { CacheProvider } from './CacheProvider' -import { useCache } from './useCache' -import { useRead } from './useRead' - -const key = (id: string) => ['key', id] as const - -const successCache = () => - cacheOptions({ - cacheKey: key('success'), - cacheFn: () => sleep(ms('0.1s')).then(() => TEXT), - }) -const failureCache = () => - cacheOptions({ - cacheKey: key('failure'), - // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors - cacheFn: () => sleep(ms('0.1s')).then(() => Promise.reject(ERROR_MESSAGE)), - }) - -const ReadSuccess = () => { - const cached = useRead(successCache()) - const cache = useCache() - - return ( - <> - {cached.data} - - - ) -} - -const ReadFailure = () => { - const cached = useRead(failureCache()) - - return <>{cached.data} -} - -describe('useRead', () => { - let cache: Cache - - beforeEach(() => { - cache = new Cache() - cache.reset() - }) - - it('should return object containing data field with only success, and It will be cached', async () => { - const { unmount } = render( - - - - - - ) - expect(screen.queryByText(FALLBACK)).toBeInTheDocument() - await waitFor(() => expect(screen.queryByText(TEXT)).toBeInTheDocument()) - - // success data cache test - unmount() - render( - - - - - - ) - expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() - expect(screen.queryByText(TEXT)).toBeInTheDocument() - }) - - it('should throw Error, and It will be cached', async () => { - const { unmount } = render( - - <>{props.error}}> - - - - - - ) - expect(screen.queryByText(FALLBACK)).toBeInTheDocument() - await waitFor(() => expect(screen.queryByText(ERROR_MESSAGE)).toBeInTheDocument()) - - // error cache test - unmount() - render( - - <>{props.error}}> - - - - - - ) - expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() - expect(screen.queryByText(ERROR_MESSAGE)).toBeInTheDocument() - }) - - it('should return object containing reset method to reset cache by key', async () => { - const { rerender } = render( - - - - - - ) - expect(screen.queryByText(FALLBACK)).toBeInTheDocument() - await waitFor(() => expect(screen.queryByText(TEXT)).toBeInTheDocument()) - const resetButton = await screen.findByRole('button', { name: 'Try again' }) - resetButton.click() - rerender( - - - - - - ) - expect(screen.queryByText(FALLBACK)).toBeInTheDocument() - await waitFor(() => expect(screen.queryByText(TEXT)).toBeInTheDocument()) - }) -}) diff --git a/packages/cache/src/useRead.ts b/packages/cache/src/useRead.ts deleted file mode 100644 index 7e9972987..000000000 --- a/packages/cache/src/useRead.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useSyncExternalStore } from 'react' -import type { ResolvedCached } from './Cache' -import type { CacheKey, CacheOptions } from './types' -import { useCache } from './useCache' - -/** - * @experimental This is experimental feature. - */ -export function useRead( - options: CacheOptions -): ResolvedCached['state'] { - const cache = useCache() - return useSyncExternalStore>( - cache.subscribe, - () => cache.suspend(options), - () => cache.suspend(options) - ).state -} diff --git a/packages/cache/src/utility-types/ExtractPartial.ts b/packages/cache/src/utility-types/ExtractPartial.ts deleted file mode 100644 index 9da388a4c..000000000 --- a/packages/cache/src/utility-types/ExtractPartial.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ExtractPartial, TExtractor extends Partial> = Extract< - TTarget, - TExtractor -> diff --git a/packages/cache/src/utility-types/Tuple.ts b/packages/cache/src/utility-types/Tuple.ts deleted file mode 100644 index 9a515a67a..000000000 --- a/packages/cache/src/utility-types/Tuple.ts +++ /dev/null @@ -1 +0,0 @@ -export type Tuple = TItem[] | readonly TItem[] diff --git a/packages/cache/src/utility-types/index.ts b/packages/cache/src/utility-types/index.ts deleted file mode 100644 index c07632b17..000000000 --- a/packages/cache/src/utility-types/index.ts +++ /dev/null @@ -1 +0,0 @@ -export type { Tuple } from './Tuple' diff --git a/packages/cache/src/utils/hashCacheKey.spec.ts b/packages/cache/src/utils/hashCacheKey.spec.ts deleted file mode 100644 index e2c663dd2..000000000 --- a/packages/cache/src/utils/hashCacheKey.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { hashCacheKey } from './hashCacheKey' - -const key1 = [ - { - field1: 'field1', - field2: 'field2', - }, -] -const key2 = [ - { - field2: 'field2', - field1: 'field1', - }, -] - -describe('JSON.stringify', () => { - it("should make different string regardless of key's field order", () => { - expect(JSON.stringify(key1) === JSON.stringify(key2)).toBe(false) - }) -}) - -describe('hashCacheKey', () => { - it("should make same string regardless of key's field order", () => { - expect(hashCacheKey(key1) === hashCacheKey(key2)).toBe(true) - }) -}) diff --git a/packages/cache/src/utils/hashCacheKey.ts b/packages/cache/src/utils/hashCacheKey.ts deleted file mode 100644 index 07202db73..000000000 --- a/packages/cache/src/utils/hashCacheKey.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { CacheKey } from '../types' -import { type PlainObject, isPlainObject } from './isPlainObject' - -export const hashCacheKey = (key: CacheKey) => - JSON.stringify(key, (_, val: unknown) => - isPlainObject(val) - ? Object.keys(val) - .sort() - .reduce((acc: PlainObject, cur) => { - acc[cur] = val[cur] - return acc - }, {}) - : val - ) diff --git a/packages/cache/src/utils/index.ts b/packages/cache/src/utils/index.ts deleted file mode 100644 index 57f4d7951..000000000 --- a/packages/cache/src/utils/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { hashCacheKey } from './hashCacheKey' diff --git a/packages/cache/src/utils/isPlainObject.spec.ts b/packages/cache/src/utils/isPlainObject.spec.ts deleted file mode 100644 index e761544b1..000000000 --- a/packages/cache/src/utils/isPlainObject.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isPlainObject } from './isPlainObject' - -describe('isPlainObject', () => { - describe('true cases', () => { - it('should return true for a plain object', () => { - expect(isPlainObject({})).toBe(true) - }) - - it('should return true for an object without a constructor', () => { - expect(isPlainObject(Object.create(null))).toBe(true) - }) - }) - - describe('false cases', () => { - it('should return false for an array', () => { - expect(isPlainObject([])).toBe(false) - }) - - it('should return false for a null value', () => { - expect(isPlainObject(null)).toBe(false) - }) - - it('should return false for an undefined value', () => { - expect(isPlainObject(undefined)).toBe(false) - }) - - it('should return false for an object instance without an Object-specific method', () => { - class Foo { - abc: any - constructor() { - this.abc = {} - } - } - expect(isPlainObject(new Foo())).toBe(false) - }) - - it('should return false for an object with a custom prototype', () => { - function Graph(this: any) { - this.vertices = [] - this.edges = [] - } - Graph.prototype.addVertex = function (v: any) { - this.vertices.push(v) - } - expect(isPlainObject(Object.create(Graph))).toBe(false) - }) - }) -}) diff --git a/packages/cache/src/utils/isPlainObject.ts b/packages/cache/src/utils/isPlainObject.ts deleted file mode 100644 index 96f0c5610..000000000 --- a/packages/cache/src/utils/isPlainObject.ts +++ /dev/null @@ -1,29 +0,0 @@ -export type PlainObject = Record - -export const isPlainObject = (value: any): value is PlainObject => { - if (!hasObjectPrototype(value)) { - return false - } - - // If has modified constructor - const ctor = value.constructor - if (typeof ctor === 'undefined') { - return true - } - - // If has modified prototype - const prot = ctor.prototype - if (!hasObjectPrototype(prot)) { - return false - } - - // If constructor does not have an Object-specific method - if (!Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf')) { - return false - } - - // Most likely a plain Object - return true -} - -const hasObjectPrototype = (value: any) => Object.prototype.toString.call(value) === '[object Object]' diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json deleted file mode 100644 index 2176a2e69..000000000 --- a/packages/cache/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "@suspensive/tsconfig/react-library.json", - "include": [".", "eslint.config.mjs"], - "compilerOptions": { - "types": ["@testing-library/jest-dom/vitest", "vitest/globals"] - } -} diff --git a/packages/cache/tsup.config.ts b/packages/cache/tsup.config.ts deleted file mode 100644 index 9560a461a..000000000 --- a/packages/cache/tsup.config.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { options } from '@suspensive/tsup' -import { defineConfig } from 'tsup' - -export default defineConfig(options) diff --git a/packages/cache/vitest.config.ts b/packages/cache/vitest.config.ts deleted file mode 100644 index fa9d99e50..000000000 --- a/packages/cache/vitest.config.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { defineConfig } from 'vitest/config' -import packageJson from './package.json' - -export default defineConfig({ - test: { - name: packageJson.name, - dir: './src', - environment: 'jsdom', - globals: true, - setupFiles: './vitest.setup.ts', - coverage: { - provider: 'istanbul', - }, - }, -}) diff --git a/packages/cache/vitest.setup.ts b/packages/cache/vitest.setup.ts deleted file mode 100644 index e87ca3f0a..000000000 --- a/packages/cache/vitest.setup.ts +++ /dev/null @@ -1,7 +0,0 @@ -import '@testing-library/jest-dom/vitest' -import { cleanup } from '@testing-library/react' -import { afterEach } from 'vitest' - -afterEach(() => { - cleanup() -}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2574f1fd5..6bdb65d45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -468,9 +468,6 @@ importers: examples/visualization: dependencies: - '@suspensive/cache': - specifier: workspace:* - version: link:../../packages/cache '@suspensive/react': specifier: workspace:* version: link:../../packages/react @@ -570,31 +567,6 @@ importers: specifier: ^15.9.0 version: 15.9.0 - packages/cache: - dependencies: - '@suspensive/utils': - specifier: workspace:* - version: link:../utils - devDependencies: - '@suspensive/eslint-config': - specifier: workspace:* - version: link:../../configs/eslint-config - '@suspensive/react': - specifier: workspace:* - version: link:../react - '@suspensive/tsconfig': - specifier: workspace:* - version: link:../../configs/tsconfig - '@suspensive/tsup': - specifier: workspace:* - version: link:../../configs/tsup - '@types/react': - specifier: catalog:react18 - version: 18.3.5 - react: - specifier: catalog:react18 - version: 18.3.1 - packages/jotai: dependencies: '@suspensive/utils':