From 3240ac2b1796efaf0cab7b589ba93e13814c0560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=9D=EC=A7=84?= Date: Thu, 11 Jul 2024 17:31:20 +0900 Subject: [PATCH 1/8] feat(promise): add SuspensePromise, useSuspensePromise, promiseOptions, PromiseCache --- packages/promise/src/PromiseCache.spec.tsx | 105 +++++++++++++++ packages/promise/src/PromiseCache.ts | 123 ++++++++++++++++++ packages/promise/src/SuspensePromise.spec.tsx | 20 +++ packages/promise/src/SuspensePromise.tsx | 19 +++ packages/promise/src/index.spec.ts | 7 - packages/promise/src/index.ts | 7 +- packages/promise/src/promiseOptions.spec.tsx | 38 ++++++ packages/promise/src/promiseOptions.ts | 8 ++ packages/promise/src/types.ts | 16 +++ .../promise/src/useSuspensePromise.spec.tsx | 94 +++++++++++++ packages/promise/src/useSuspensePromise.ts | 26 ++++ packages/promise/src/utility-types/Tuple.ts | 1 + packages/promise/src/utility-types/index.ts | 1 + .../src/utils/hasResetKeysChanged.spec.ts | 57 ++++++++ .../promise/src/utils/hasResetKeysChanged.ts | 2 + packages/promise/src/utils/hashKey.spec.ts | 26 ++++ packages/promise/src/utils/hashKey.ts | 14 ++ packages/promise/src/utils/index.ts | 2 + .../promise/src/utils/isPlainObject.spec.ts | 48 +++++++ packages/promise/src/utils/isPlainObject.ts | 29 +++++ 20 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 packages/promise/src/PromiseCache.spec.tsx create mode 100644 packages/promise/src/PromiseCache.ts create mode 100644 packages/promise/src/SuspensePromise.spec.tsx create mode 100644 packages/promise/src/SuspensePromise.tsx delete mode 100644 packages/promise/src/index.spec.ts create mode 100644 packages/promise/src/promiseOptions.spec.tsx create mode 100644 packages/promise/src/promiseOptions.ts create mode 100644 packages/promise/src/types.ts create mode 100644 packages/promise/src/useSuspensePromise.spec.tsx create mode 100644 packages/promise/src/useSuspensePromise.ts create mode 100644 packages/promise/src/utility-types/Tuple.ts create mode 100644 packages/promise/src/utility-types/index.ts create mode 100644 packages/promise/src/utils/hasResetKeysChanged.spec.ts create mode 100644 packages/promise/src/utils/hasResetKeysChanged.ts create mode 100644 packages/promise/src/utils/hashKey.spec.ts create mode 100644 packages/promise/src/utils/hashKey.ts create mode 100644 packages/promise/src/utils/index.ts create mode 100644 packages/promise/src/utils/isPlainObject.spec.ts create mode 100644 packages/promise/src/utils/isPlainObject.ts diff --git a/packages/promise/src/PromiseCache.spec.tsx b/packages/promise/src/PromiseCache.spec.tsx new file mode 100644 index 000000000..63d323a5e --- /dev/null +++ b/packages/promise/src/PromiseCache.spec.tsx @@ -0,0 +1,105 @@ +import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/test-utils' +import { render, screen, waitFor } from '@testing-library/react' +import ms from 'ms' +import { Suspense } from 'react' +import { promiseCache } from './PromiseCache' +import { SuspensePromise } from './SuspensePromise' +import { hashKey } from './utils' + +const key = (id: number) => ['key', id] as const + +// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors +const asyncErrorFn = () => new Promise((_, reject) => reject(ERROR_MESSAGE)) +describe('promiseCache', () => { + beforeEach(() => promiseCache.reset()) + + it("have clearError method without key should clear promise & error for all key's promiseCacheState", async () => { + expect(promiseCache.getError(key(1))).toBeUndefined() + expect(promiseCache.getError(key(2))).toBeUndefined() + try { + promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + } catch (promiseToSuspense) { + expect(await promiseToSuspense).toBeUndefined() + } + try { + promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + } catch (error) { + expect(error).toBe(ERROR_MESSAGE) + } + try { + promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + } catch (promiseToSuspense) { + expect(await promiseToSuspense).toBeUndefined() + } + try { + promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + } catch (error) { + expect(error).toBe(ERROR_MESSAGE) + } + expect(promiseCache.getError(key(1))).toBe(ERROR_MESSAGE) + expect(promiseCache.getError(key(2))).toBe(ERROR_MESSAGE) + + promiseCache.clearError() + expect(promiseCache.getError(key(1))).toBeUndefined() + expect(promiseCache.getError(key(2))).toBeUndefined() + }) + + it("have clearError method with key should clear promise & error for key's promiseCacheState", async () => { + expect(promiseCache.getError(key(1))).toBeUndefined() + expect(promiseCache.getError(key(2))).toBeUndefined() + try { + promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + } catch (promiseToSuspense) { + expect(await promiseToSuspense).toBeUndefined() + } + try { + promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + } catch (error) { + expect(error).toBe(ERROR_MESSAGE) + } + try { + promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + } catch (promiseToSuspense) { + expect(await promiseToSuspense).toBeUndefined() + } + try { + promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + } catch (error) { + expect(error).toBe(ERROR_MESSAGE) + } + expect(promiseCache.getError(key(1))).toBe(ERROR_MESSAGE) + expect(promiseCache.getError(key(2))).toBe(ERROR_MESSAGE) + + promiseCache.clearError(key(1)) + expect(promiseCache.getError(key(1))).toBeUndefined() + expect(promiseCache.getError(key(2))).toBe(ERROR_MESSAGE) + promiseCache.clearError(key(2)) + expect(promiseCache.getError(key(1))).toBeUndefined() + expect(promiseCache.getError(key(2))).toBeUndefined() + }) + + it("have getData method with key should get data of key's promiseCacheState", async () => { + render( + + sleep(ms('0.1s')).then(() => TEXT) }}> + {(resolvedData) => <>{resolvedData.data}} + + + ) + + expect(screen.queryByText(FALLBACK)).toBeInTheDocument() + expect(screen.queryByText(TEXT)).not.toBeInTheDocument() + expect(promiseCache.getData(key(1))).toBeUndefined() + await waitFor(() => expect(screen.queryByText(TEXT)).toBeInTheDocument()) + expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() + expect(promiseCache.getData(key(1))).toBe(TEXT) + }) + + it('should handle unsubscribe gracefully when no subscribers exist', () => { + const mockSync = vi.fn() + const key = ['nonexistent', 'key'] as const + promiseCache.unsubscribe(key, mockSync) + + expect(promiseCache['syncsMap'].get(hashKey(key))).toBeUndefined() + }) +}) diff --git a/packages/promise/src/PromiseCache.ts b/packages/promise/src/PromiseCache.ts new file mode 100644 index 000000000..9c39c157a --- /dev/null +++ b/packages/promise/src/PromiseCache.ts @@ -0,0 +1,123 @@ +import type { Key, SuspensePromiseOptions } from './types' +import { hashKey } from './utils' + +type Sync = (...args: unknown[]) => unknown + +type PromiseCacheState = { + promise?: Promise + key: TKey + hashedKey: ReturnType + error?: unknown + data?: unknown +} + +class PromiseCache { + private cache = new Map, PromiseCacheState>() + private syncsMap = new Map, Sync[]>() + + public reset = (key?: Key) => { + if (key === undefined || key.length === 0) { + this.cache.clear() + this.syncSubscribers() + return + } + + const hashedKey = hashKey(key) + + if (this.cache.has(hashedKey)) { + // TODO: reset with key index hierarchy + this.cache.delete(hashedKey) + } + + this.syncSubscribers(key) + } + + public clearError = (key?: Key) => { + if (key === undefined || key.length === 0) { + this.cache.forEach((value, key, map) => { + map.set(key, { ...value, promise: undefined, error: undefined }) + }) + return + } + + const hashedKey = hashKey(key) + const promiseCacheState = this.cache.get(hashedKey) + if (promiseCacheState) { + // TODO: clearError with key index hierarchy + this.cache.set(hashedKey, { ...promiseCacheState, promise: undefined, error: undefined }) + } + } + + public suspend = ({ key, fn }: SuspensePromiseOptions): TData => { + const hashedKey = hashKey(key) + const promiseCacheState = this.cache.get(hashedKey) + + if (promiseCacheState?.error) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw promiseCacheState.error + } + if (promiseCacheState?.data) { + return promiseCacheState.data as TData + } + + if (promiseCacheState?.promise) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw promiseCacheState.promise + } + + const newPromiseCache: PromiseCacheState = { + key, + hashedKey, + promise: fn({ key }) + .then((data) => { + newPromiseCache.data = data + }) + .catch((error: unknown) => { + newPromiseCache.error = error + }), + } + + this.cache.set(hashedKey, newPromiseCache) + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw newPromiseCache.promise + } + + public getData = (key: Key) => this.cache.get(hashKey(key))?.data + public getError = (key: Key) => this.cache.get(hashKey(key))?.error + + public subscribe(key: Key, syncSubscriber: Sync) { + const hashedKey = hashKey(key) + const syncs = this.syncsMap.get(hashedKey) + this.syncsMap.set(hashedKey, [...(syncs ?? []), syncSubscriber]) + + const subscribed = { + unsubscribe: () => this.unsubscribe(key, syncSubscriber), + } + return subscribed + } + + public unsubscribe(key: Key, syncSubscriber: Sync) { + const hashedKey = hashKey(key) + const syncs = this.syncsMap.get(hashedKey) + + if (syncs) { + this.syncsMap.set( + hashedKey, + syncs.filter((sync) => sync !== syncSubscriber) + ) + } + } + + private syncSubscribers = (key?: Key) => { + const hashedKey = key ? hashKey(key) : undefined + + return hashedKey + ? this.syncsMap.get(hashedKey)?.forEach((sync) => sync()) + : this.syncsMap.forEach((syncs) => syncs.forEach((sync) => sync())) + } +} + +/** + * @experimental This is experimental feature. + */ +export const promiseCache = new PromiseCache() diff --git a/packages/promise/src/SuspensePromise.spec.tsx b/packages/promise/src/SuspensePromise.spec.tsx new file mode 100644 index 000000000..d547837d6 --- /dev/null +++ b/packages/promise/src/SuspensePromise.spec.tsx @@ -0,0 +1,20 @@ +import { Suspense } from '@suspensive/react' +import { TEXT } from '@suspensive/test-utils' +import { render, screen } from '@testing-library/react' +import { SuspensePromise } from './SuspensePromise' + +const key = (id: number) => ['key', id] as const + +describe('', () => { + it('should render child component with data from useSuspensePromise hook', async () => { + render( + + Promise.resolve(TEXT) }}> + {({ data }) => <>{data}} + + + ) + + expect(await screen.findByText(TEXT)).toBeInTheDocument() + }) +}) diff --git a/packages/promise/src/SuspensePromise.tsx b/packages/promise/src/SuspensePromise.tsx new file mode 100644 index 000000000..c1e22b750 --- /dev/null +++ b/packages/promise/src/SuspensePromise.tsx @@ -0,0 +1,19 @@ +import { type FunctionComponent } from 'react' +import type { Key, ResolvedData, SuspensePromiseOptions } from './types' +import { useSuspensePromise } from './useSuspensePromise' + +/** + * @experimental This is experimental feature. + */ +export type SuspensePromiseProps = { + options: SuspensePromiseOptions + children: FunctionComponent> +} + +/** + * @experimental This is experimental feature. + */ +export const SuspensePromise = ({ + children: Children, + options, +}: SuspensePromiseProps) => (options)} /> diff --git a/packages/promise/src/index.spec.ts b/packages/promise/src/index.spec.ts deleted file mode 100644 index ed04d7a91..000000000 --- a/packages/promise/src/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { test } from '.' - -describe('test', () => { - it('should be test', () => { - expect(test).toBe('test') - }) -}) diff --git a/packages/promise/src/index.ts b/packages/promise/src/index.ts index 357438bed..2f9b8add7 100644 --- a/packages/promise/src/index.ts +++ b/packages/promise/src/index.ts @@ -1 +1,6 @@ -export const test = 'test' +export { SuspensePromise } from './SuspensePromise' +export { useSuspensePromise } from './useSuspensePromise' +export { promiseOptions } from './promiseOptions' + +export type { SuspensePromiseProps } from './SuspensePromise' +export type { SuspensePromiseOptions } from './types' diff --git a/packages/promise/src/promiseOptions.spec.tsx b/packages/promise/src/promiseOptions.spec.tsx new file mode 100644 index 000000000..404d1fb70 --- /dev/null +++ b/packages/promise/src/promiseOptions.spec.tsx @@ -0,0 +1,38 @@ +import { FALLBACK, TEXT } from '@suspensive/test-utils' +import { render, screen } from '@testing-library/react' +import { Suspense } from 'react' +import { promiseOptions } from './promiseOptions' +import { SuspensePromise } from './SuspensePromise' +import { useSuspensePromise } from './useSuspensePromise' + +const key = (id: number) => ['key', id] as const + +const options = promiseOptions({ key: key(1), fn: () => Promise.resolve(TEXT) }) + +describe('promiseOptions', () => { + it('should be used with SuspensePromise', async () => { + render( + + {({ data }) => <>{data}} + + ) + + expect(await screen.findByText(TEXT)).toBeInTheDocument() + }) + + it('should be used with useSuspensePromise', async () => { + const SuspensePromiseComponent = () => { + const resolvedData = useSuspensePromise(options) + + return <>{resolvedData.data} + } + + render( + + + + ) + + expect(await screen.findByText(TEXT)).toBeInTheDocument() + }) +}) diff --git a/packages/promise/src/promiseOptions.ts b/packages/promise/src/promiseOptions.ts new file mode 100644 index 000000000..4470ccaf4 --- /dev/null +++ b/packages/promise/src/promiseOptions.ts @@ -0,0 +1,8 @@ +import type { Key, SuspensePromiseOptions } from './types' + +/** + * @experimental This is experimental feature. + */ +export const promiseOptions = (options: SuspensePromiseOptions) => { + return options +} diff --git a/packages/promise/src/types.ts b/packages/promise/src/types.ts new file mode 100644 index 000000000..00e806416 --- /dev/null +++ b/packages/promise/src/types.ts @@ -0,0 +1,16 @@ +import type { Tuple } from './utility-types' + +export type Key = Tuple + +/** + * @experimental This is experimental feature. + */ +export type SuspensePromiseOptions = { + key: TKey + fn: (options: { key: TKey }) => Promise +} + +export type ResolvedData = { + data: TData + reset: () => void +} diff --git a/packages/promise/src/useSuspensePromise.spec.tsx b/packages/promise/src/useSuspensePromise.spec.tsx new file mode 100644 index 000000000..8fd553bba --- /dev/null +++ b/packages/promise/src/useSuspensePromise.spec.tsx @@ -0,0 +1,94 @@ +import { ErrorBoundary, Suspense } from '@suspensive/react' +import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/test-utils' +import { render, screen, waitFor } from '@testing-library/react' +import ms from 'ms' +import { promiseCache } from './PromiseCache' +import { useSuspensePromise } from './useSuspensePromise' + +const key = (id: number) => ['key', id] as const + +const SuspensePromiseSuccess = () => { + const resolvedData = useSuspensePromise({ key: key(1), fn: () => sleep(ms('0.1s')).then(() => TEXT) }) + + return ( + <> + {resolvedData.data} + + + ) +} + +const SuspensePromiseFailure = () => { + const resolvedData = useSuspensePromise({ + key: key(1), + fn: () => sleep(ms('0.1s')).then(() => Promise.reject(new Error(ERROR_MESSAGE))), + }) + + return <>{resolvedData.data} +} + +describe('useSuspensePromise', () => { + beforeEach(() => promiseCache.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.message}}> + + + + + ) + expect(screen.queryByText(FALLBACK)).toBeInTheDocument() + await waitFor(() => expect(screen.queryByText(ERROR_MESSAGE)).toBeInTheDocument()) + + // error cache test + unmount() + render( + <>{props.error.message}}> + + + + + ) + 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/promise/src/useSuspensePromise.ts b/packages/promise/src/useSuspensePromise.ts new file mode 100644 index 000000000..87f1fc71d --- /dev/null +++ b/packages/promise/src/useSuspensePromise.ts @@ -0,0 +1,26 @@ +import { useMemo, useSyncExternalStore } from 'react' +import { promiseCache } from './PromiseCache' +import type { Key, ResolvedData, SuspensePromiseOptions } from './types' + +/** + * @experimental This is experimental feature. + */ +export const useSuspensePromise = ( + options: SuspensePromiseOptions +): ResolvedData => { + const syncData = () => promiseCache.suspend(options) + const data = useSyncExternalStore( + (sync) => promiseCache.subscribe(options.key, sync).unsubscribe, + syncData, + syncData + ) + + return useMemo( + () => ({ + key: options.key, + data, + reset: () => promiseCache.reset(options.key), + }), + [data, options.key] + ) +} diff --git a/packages/promise/src/utility-types/Tuple.ts b/packages/promise/src/utility-types/Tuple.ts new file mode 100644 index 000000000..9a515a67a --- /dev/null +++ b/packages/promise/src/utility-types/Tuple.ts @@ -0,0 +1 @@ +export type Tuple = TItem[] | readonly TItem[] diff --git a/packages/promise/src/utility-types/index.ts b/packages/promise/src/utility-types/index.ts new file mode 100644 index 000000000..c07632b17 --- /dev/null +++ b/packages/promise/src/utility-types/index.ts @@ -0,0 +1 @@ +export type { Tuple } from './Tuple' diff --git a/packages/promise/src/utils/hasResetKeysChanged.spec.ts b/packages/promise/src/utils/hasResetKeysChanged.spec.ts new file mode 100644 index 000000000..fbb526970 --- /dev/null +++ b/packages/promise/src/utils/hasResetKeysChanged.spec.ts @@ -0,0 +1,57 @@ +import { hasResetKeysChanged } from './hasResetKeysChanged' + +const primitive = 0 +const reference1 = { test: 0 } +const reference2 = { test: 0 } + +describe('hasResetKeysChanged', () => { + describe('true cases', () => { + it('should return true if the two arrays have different lengths.', () => { + const array1 = Array.from({ length: 1 }).map((_, index) => primitive + index) + const array2 = Array.from({ length: 2 }).map((_, index) => primitive + index) + + expect(hasResetKeysChanged(array1, array2)).toBe(true) + }) + + it('should return true if the two arrays have same lengths but at least one primitive element is different in each index of arrays.', () => { + const array1 = [primitive, primitive + 1] + const array2 = [primitive, primitive] + + expect(hasResetKeysChanged(array1, array2)).toBe(true) + }) + + it('should return true if the two arrays have same lengths and have all same primitive elements but order is different', () => { + const array1 = [primitive, primitive + 1] + const array2 = [primitive + 1, primitive] + + expect(hasResetKeysChanged(array1, array2)).toBe(true) + }) + + it('should return true when two arrays have each references elements in index of array have different references', () => { + const array1 = [reference1, { test: 2 }] + const array2 = [reference1, { test: 2 }] + + expect(hasResetKeysChanged(array1, array2)).toBe(true) + }) + }) + + describe('false cases', () => { + it('should return false if the two arrays have same lengths and same primitive element in each index of arrays.', () => { + const array1 = Array.from({ length: 1 }).map((_, index) => primitive + index) + const array2 = Array.from({ length: 1 }).map((_, index) => primitive + index) + + expect(hasResetKeysChanged(array1, array2)).toBe(false) + }) + + it('should return false when two arrays have each references elements in index of array have same references', () => { + const array1 = [reference1, reference2] + const array2 = [reference1, reference2] + + expect(hasResetKeysChanged(array1, array2)).toBe(false) + }) + + it('should return false when no arrays as parameters. because of default value', () => { + expect(hasResetKeysChanged()).toBe(false) + }) + }) +}) diff --git a/packages/promise/src/utils/hasResetKeysChanged.ts b/packages/promise/src/utils/hasResetKeysChanged.ts new file mode 100644 index 000000000..12c1d147f --- /dev/null +++ b/packages/promise/src/utils/hasResetKeysChanged.ts @@ -0,0 +1,2 @@ +export const hasResetKeysChanged = (a: unknown[] = [], b: unknown[] = []) => + a.length !== b.length || a.some((item, index) => !Object.is(item, b[index])) diff --git a/packages/promise/src/utils/hashKey.spec.ts b/packages/promise/src/utils/hashKey.spec.ts new file mode 100644 index 000000000..8f6bf57a2 --- /dev/null +++ b/packages/promise/src/utils/hashKey.spec.ts @@ -0,0 +1,26 @@ +import { hashKey } from './hashKey' + +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('hashKey', () => { + it("should make same string regardless of key's field order", () => { + expect(hashKey(key1) === hashKey(key2)).toBe(true) + }) +}) diff --git a/packages/promise/src/utils/hashKey.ts b/packages/promise/src/utils/hashKey.ts new file mode 100644 index 000000000..8eafb624c --- /dev/null +++ b/packages/promise/src/utils/hashKey.ts @@ -0,0 +1,14 @@ +import type { Key } from '../types' +import { type PlainObject, isPlainObject } from './isPlainObject' + +export const hashKey = (key: Key) => + 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/promise/src/utils/index.ts b/packages/promise/src/utils/index.ts new file mode 100644 index 000000000..2f26201ab --- /dev/null +++ b/packages/promise/src/utils/index.ts @@ -0,0 +1,2 @@ +export { hasResetKeysChanged } from './hasResetKeysChanged' +export { hashKey } from './hashKey' diff --git a/packages/promise/src/utils/isPlainObject.spec.ts b/packages/promise/src/utils/isPlainObject.spec.ts new file mode 100644 index 000000000..e761544b1 --- /dev/null +++ b/packages/promise/src/utils/isPlainObject.spec.ts @@ -0,0 +1,48 @@ +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/promise/src/utils/isPlainObject.ts b/packages/promise/src/utils/isPlainObject.ts new file mode 100644 index 000000000..96f0c5610 --- /dev/null +++ b/packages/promise/src/utils/isPlainObject.ts @@ -0,0 +1,29 @@ +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]' From 20a2a052d787c7fec4c4089aac48770eb94c6f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=9D=EC=A7=84?= Date: Thu, 11 Jul 2024 18:30:36 +0900 Subject: [PATCH 2/8] feat(promise): add PromiseCacheProvider --- packages/promise/src/PromiseCache.spec.tsx | 22 ++++-- packages/promise/src/PromiseCache.ts | 10 ++- packages/promise/src/PromiseCacheProvider.tsx | 12 ++++ packages/promise/src/SuspensePromise.spec.tsx | 20 ++++-- .../src/contexts/PromiseCacheContext.ts | 4 ++ packages/promise/src/contexts/index.ts | 1 + packages/promise/src/index.ts | 3 + packages/promise/src/promiseOptions.spec.tsx | 24 +++++-- packages/promise/src/usePromiseCache.ts | 15 +++++ .../promise/src/useSuspensePromise.spec.tsx | 67 ++++++++++++------- packages/promise/src/useSuspensePromise.ts | 3 +- 11 files changed, 132 insertions(+), 49 deletions(-) create mode 100644 packages/promise/src/PromiseCacheProvider.tsx create mode 100644 packages/promise/src/contexts/PromiseCacheContext.ts create mode 100644 packages/promise/src/contexts/index.ts create mode 100644 packages/promise/src/usePromiseCache.ts diff --git a/packages/promise/src/PromiseCache.spec.tsx b/packages/promise/src/PromiseCache.spec.tsx index 63d323a5e..d354a670c 100644 --- a/packages/promise/src/PromiseCache.spec.tsx +++ b/packages/promise/src/PromiseCache.spec.tsx @@ -2,7 +2,8 @@ import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/test-utils' import { render, screen, waitFor } from '@testing-library/react' import ms from 'ms' import { Suspense } from 'react' -import { promiseCache } from './PromiseCache' +import { PromiseCache } from './PromiseCache' +import { PromiseCacheProvider } from './PromiseCacheProvider' import { SuspensePromise } from './SuspensePromise' import { hashKey } from './utils' @@ -11,7 +12,12 @@ const key = (id: number) => ['key', id] as const // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors const asyncErrorFn = () => new Promise((_, reject) => reject(ERROR_MESSAGE)) describe('promiseCache', () => { - beforeEach(() => promiseCache.reset()) + let promiseCache: PromiseCache + + beforeEach(() => { + promiseCache = new PromiseCache() + promiseCache.reset() + }) it("have clearError method without key should clear promise & error for all key's promiseCacheState", async () => { expect(promiseCache.getError(key(1))).toBeUndefined() @@ -80,11 +86,13 @@ describe('promiseCache', () => { it("have getData method with key should get data of key's promiseCacheState", async () => { render( - - sleep(ms('0.1s')).then(() => TEXT) }}> - {(resolvedData) => <>{resolvedData.data}} - - + + + sleep(ms('0.1s')).then(() => TEXT) }}> + {(resolvedData) => <>{resolvedData.data}} + + + ) expect(screen.queryByText(FALLBACK)).toBeInTheDocument() diff --git a/packages/promise/src/PromiseCache.ts b/packages/promise/src/PromiseCache.ts index 9c39c157a..9dab1e28d 100644 --- a/packages/promise/src/PromiseCache.ts +++ b/packages/promise/src/PromiseCache.ts @@ -11,7 +11,10 @@ type PromiseCacheState = { data?: unknown } -class PromiseCache { +/** + * @experimental This is experimental feature. + */ +export class PromiseCache { private cache = new Map, PromiseCacheState>() private syncsMap = new Map, Sync[]>() @@ -116,8 +119,3 @@ class PromiseCache { : this.syncsMap.forEach((syncs) => syncs.forEach((sync) => sync())) } } - -/** - * @experimental This is experimental feature. - */ -export const promiseCache = new PromiseCache() diff --git a/packages/promise/src/PromiseCacheProvider.tsx b/packages/promise/src/PromiseCacheProvider.tsx new file mode 100644 index 000000000..61e80b721 --- /dev/null +++ b/packages/promise/src/PromiseCacheProvider.tsx @@ -0,0 +1,12 @@ +import { type PropsWithChildren } from 'react' +import { PromiseCacheContext } from './contexts' +import type { PromiseCache } from './PromiseCache' + +type PromiseCacheProviderProps = PropsWithChildren<{ cache: PromiseCache }> + +/** + * @experimental This is experimental feature. + */ +export const PromiseCacheProvider = ({ cache, children }: PromiseCacheProviderProps) => ( + {children} +) diff --git a/packages/promise/src/SuspensePromise.spec.tsx b/packages/promise/src/SuspensePromise.spec.tsx index d547837d6..635012bd7 100644 --- a/packages/promise/src/SuspensePromise.spec.tsx +++ b/packages/promise/src/SuspensePromise.spec.tsx @@ -1,18 +1,28 @@ import { Suspense } from '@suspensive/react' import { TEXT } from '@suspensive/test-utils' import { render, screen } from '@testing-library/react' +import { PromiseCache } from './PromiseCache' +import { PromiseCacheProvider } from './PromiseCacheProvider' import { SuspensePromise } from './SuspensePromise' const key = (id: number) => ['key', id] as const describe('', () => { + let promiseCache: PromiseCache + + beforeEach(() => { + promiseCache = new PromiseCache() + }) + it('should render child component with data from useSuspensePromise hook', async () => { render( - - Promise.resolve(TEXT) }}> - {({ data }) => <>{data}} - - + + + Promise.resolve(TEXT) }}> + {({ data }) => <>{data}} + + + ) expect(await screen.findByText(TEXT)).toBeInTheDocument() diff --git a/packages/promise/src/contexts/PromiseCacheContext.ts b/packages/promise/src/contexts/PromiseCacheContext.ts new file mode 100644 index 000000000..40e495989 --- /dev/null +++ b/packages/promise/src/contexts/PromiseCacheContext.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react' +import type { PromiseCache } from '../PromiseCache' + +export const PromiseCacheContext = createContext(null) diff --git a/packages/promise/src/contexts/index.ts b/packages/promise/src/contexts/index.ts new file mode 100644 index 000000000..37e00115c --- /dev/null +++ b/packages/promise/src/contexts/index.ts @@ -0,0 +1 @@ +export { PromiseCacheContext } from './PromiseCacheContext' diff --git a/packages/promise/src/index.ts b/packages/promise/src/index.ts index 2f9b8add7..8cd7a2c73 100644 --- a/packages/promise/src/index.ts +++ b/packages/promise/src/index.ts @@ -1,6 +1,9 @@ export { SuspensePromise } from './SuspensePromise' export { useSuspensePromise } from './useSuspensePromise' export { promiseOptions } from './promiseOptions' +export { PromiseCache } from './PromiseCache' +export { PromiseCacheProvider } from './PromiseCacheProvider' +export { usePromiseCache } from './usePromiseCache' export type { SuspensePromiseProps } from './SuspensePromise' export type { SuspensePromiseOptions } from './types' diff --git a/packages/promise/src/promiseOptions.spec.tsx b/packages/promise/src/promiseOptions.spec.tsx index 404d1fb70..cfad69af7 100644 --- a/packages/promise/src/promiseOptions.spec.tsx +++ b/packages/promise/src/promiseOptions.spec.tsx @@ -1,6 +1,8 @@ import { FALLBACK, TEXT } from '@suspensive/test-utils' import { render, screen } from '@testing-library/react' import { Suspense } from 'react' +import { PromiseCache } from './PromiseCache' +import { PromiseCacheProvider } from './PromiseCacheProvider' import { promiseOptions } from './promiseOptions' import { SuspensePromise } from './SuspensePromise' import { useSuspensePromise } from './useSuspensePromise' @@ -10,11 +12,19 @@ const key = (id: number) => ['key', id] as const const options = promiseOptions({ key: key(1), fn: () => Promise.resolve(TEXT) }) describe('promiseOptions', () => { + let promiseCache: PromiseCache + + beforeEach(() => { + promiseCache = new PromiseCache() + }) + it('should be used with SuspensePromise', async () => { render( - - {({ data }) => <>{data}} - + + + {({ data }) => <>{data}} + + ) expect(await screen.findByText(TEXT)).toBeInTheDocument() @@ -28,9 +38,11 @@ describe('promiseOptions', () => { } render( - - - + + + + + ) expect(await screen.findByText(TEXT)).toBeInTheDocument() diff --git a/packages/promise/src/usePromiseCache.ts b/packages/promise/src/usePromiseCache.ts new file mode 100644 index 000000000..bcc396ae1 --- /dev/null +++ b/packages/promise/src/usePromiseCache.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react' +import { PromiseCacheContext } from './contexts' + +/** + * @experimental This is experimental feature. + */ +export const usePromiseCache = () => { + const promiseCache = useContext(PromiseCacheContext) + + if (promiseCache == null) { + throw new Error('PromiseCacheProvider should be in parent') + } + + return promiseCache +} diff --git a/packages/promise/src/useSuspensePromise.spec.tsx b/packages/promise/src/useSuspensePromise.spec.tsx index 8fd553bba..d44e92fc5 100644 --- a/packages/promise/src/useSuspensePromise.spec.tsx +++ b/packages/promise/src/useSuspensePromise.spec.tsx @@ -2,7 +2,8 @@ import { ErrorBoundary, Suspense } from '@suspensive/react' import { ERROR_MESSAGE, FALLBACK, TEXT, sleep } from '@suspensive/test-utils' import { render, screen, waitFor } from '@testing-library/react' import ms from 'ms' -import { promiseCache } from './PromiseCache' +import { PromiseCache } from './PromiseCache' +import { PromiseCacheProvider } from './PromiseCacheProvider' import { useSuspensePromise } from './useSuspensePromise' const key = (id: number) => ['key', id] as const @@ -28,12 +29,20 @@ const SuspensePromiseFailure = () => { } describe('useSuspensePromise', () => { - beforeEach(() => promiseCache.reset()) + let promiseCache: PromiseCache + + beforeEach(() => { + promiseCache = new PromiseCache() + promiseCache.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()) @@ -41,9 +50,11 @@ describe('useSuspensePromise', () => { // success data cache test unmount() render( - - - + + + + + ) expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() expect(screen.queryByText(TEXT)).toBeInTheDocument() @@ -51,11 +62,13 @@ describe('useSuspensePromise', () => { it('should throw Error, and It will be cached', async () => { const { unmount } = render( - <>{props.error.message}}> - - - - + + <>{props.error.message}}> + + + + + ) expect(screen.queryByText(FALLBACK)).toBeInTheDocument() await waitFor(() => expect(screen.queryByText(ERROR_MESSAGE)).toBeInTheDocument()) @@ -63,11 +76,13 @@ describe('useSuspensePromise', () => { // error cache test unmount() render( - <>{props.error.message}}> - - - - + + <>{props.error.message}}> + + + + + ) expect(screen.queryByText(FALLBACK)).not.toBeInTheDocument() expect(screen.queryByText(ERROR_MESSAGE)).toBeInTheDocument() @@ -75,18 +90,22 @@ describe('useSuspensePromise', () => { 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/promise/src/useSuspensePromise.ts b/packages/promise/src/useSuspensePromise.ts index 87f1fc71d..1d0f88b96 100644 --- a/packages/promise/src/useSuspensePromise.ts +++ b/packages/promise/src/useSuspensePromise.ts @@ -1,6 +1,6 @@ import { useMemo, useSyncExternalStore } from 'react' -import { promiseCache } from './PromiseCache' import type { Key, ResolvedData, SuspensePromiseOptions } from './types' +import { usePromiseCache } from './usePromiseCache' /** * @experimental This is experimental feature. @@ -8,6 +8,7 @@ import type { Key, ResolvedData, SuspensePromiseOptions } from './types' export const useSuspensePromise = ( options: SuspensePromiseOptions ): ResolvedData => { + const promiseCache = usePromiseCache() const syncData = () => promiseCache.suspend(options) const data = useSyncExternalStore( (sync) => promiseCache.subscribe(options.key, sync).unsubscribe, From 6d30882194a059a0e923822fae53605f8939f7ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=9D=EC=A7=84?= Date: Sat, 13 Jul 2024 22:24:19 +0900 Subject: [PATCH 3/8] fix(promise): fix key -> promiseKey, fn -> promiseFn --- packages/promise/src/PromiseCache.spec.tsx | 18 ++--- packages/promise/src/PromiseCache.ts | 70 ++++++++++--------- packages/promise/src/SuspensePromise.spec.tsx | 2 +- packages/promise/src/promiseOptions.spec.tsx | 2 +- packages/promise/src/types.ts | 4 +- .../promise/src/useSuspensePromise.spec.tsx | 6 +- packages/promise/src/useSuspensePromise.ts | 8 +-- 7 files changed, 57 insertions(+), 53 deletions(-) diff --git a/packages/promise/src/PromiseCache.spec.tsx b/packages/promise/src/PromiseCache.spec.tsx index d354a670c..90981c892 100644 --- a/packages/promise/src/PromiseCache.spec.tsx +++ b/packages/promise/src/PromiseCache.spec.tsx @@ -23,22 +23,22 @@ describe('promiseCache', () => { expect(promiseCache.getError(key(1))).toBeUndefined() expect(promiseCache.getError(key(2))).toBeUndefined() try { - promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(1), promiseFn: asyncErrorFn }) } catch (promiseToSuspense) { expect(await promiseToSuspense).toBeUndefined() } try { - promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(1), promiseFn: asyncErrorFn }) } catch (error) { expect(error).toBe(ERROR_MESSAGE) } try { - promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(2), promiseFn: asyncErrorFn }) } catch (promiseToSuspense) { expect(await promiseToSuspense).toBeUndefined() } try { - promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(2), promiseFn: asyncErrorFn }) } catch (error) { expect(error).toBe(ERROR_MESSAGE) } @@ -54,22 +54,22 @@ describe('promiseCache', () => { expect(promiseCache.getError(key(1))).toBeUndefined() expect(promiseCache.getError(key(2))).toBeUndefined() try { - promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(1), promiseFn: asyncErrorFn }) } catch (promiseToSuspense) { expect(await promiseToSuspense).toBeUndefined() } try { - promiseCache.suspend({ key: key(1), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(1), promiseFn: asyncErrorFn }) } catch (error) { expect(error).toBe(ERROR_MESSAGE) } try { - promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(2), promiseFn: asyncErrorFn }) } catch (promiseToSuspense) { expect(await promiseToSuspense).toBeUndefined() } try { - promiseCache.suspend({ key: key(2), fn: asyncErrorFn }) + promiseCache.suspend({ promiseKey: key(2), promiseFn: asyncErrorFn }) } catch (error) { expect(error).toBe(ERROR_MESSAGE) } @@ -88,7 +88,7 @@ describe('promiseCache', () => { render( - sleep(ms('0.1s')).then(() => TEXT) }}> + sleep(ms('0.1s')).then(() => TEXT) }}> {(resolvedData) => <>{resolvedData.data}} diff --git a/packages/promise/src/PromiseCache.ts b/packages/promise/src/PromiseCache.ts index 9dab1e28d..05c280853 100644 --- a/packages/promise/src/PromiseCache.ts +++ b/packages/promise/src/PromiseCache.ts @@ -5,7 +5,7 @@ type Sync = (...args: unknown[]) => unknown type PromiseCacheState = { promise?: Promise - key: TKey + promiseKey: TKey hashedKey: ReturnType error?: unknown data?: unknown @@ -18,32 +18,32 @@ export class PromiseCache { private cache = new Map, PromiseCacheState>() private syncsMap = new Map, Sync[]>() - public reset = (key?: Key) => { - if (key === undefined || key.length === 0) { + public reset = (promiseKey?: Key) => { + if (promiseKey === undefined || promiseKey.length === 0) { this.cache.clear() this.syncSubscribers() return } - const hashedKey = hashKey(key) + const hashedKey = hashKey(promiseKey) if (this.cache.has(hashedKey)) { // TODO: reset with key index hierarchy this.cache.delete(hashedKey) } - this.syncSubscribers(key) + this.syncSubscribers(promiseKey) } - public clearError = (key?: Key) => { - if (key === undefined || key.length === 0) { + public clearError = (promiseKey?: Key) => { + if (promiseKey === undefined || promiseKey.length === 0) { this.cache.forEach((value, key, map) => { map.set(key, { ...value, promise: undefined, error: undefined }) }) return } - const hashedKey = hashKey(key) + const hashedKey = hashKey(promiseKey) const promiseCacheState = this.cache.get(hashedKey) if (promiseCacheState) { // TODO: clearError with key index hierarchy @@ -51,27 +51,31 @@ export class PromiseCache { } } - public suspend = ({ key, fn }: SuspensePromiseOptions): TData => { - const hashedKey = hashKey(key) + public suspend = ({ + promiseKey, + promiseFn, + }: SuspensePromiseOptions): TData => { + const hashedKey = hashKey(promiseKey) const promiseCacheState = this.cache.get(hashedKey) - if (promiseCacheState?.error) { - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw promiseCacheState.error - } - if (promiseCacheState?.data) { - return promiseCacheState.data as TData - } - - if (promiseCacheState?.promise) { - // eslint-disable-next-line @typescript-eslint/no-throw-literal - throw promiseCacheState.promise + if (promiseCacheState) { + if (promiseCacheState.error) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw promiseCacheState.error + } + if (promiseCacheState.data) { + return promiseCacheState.data as TData + } + + if (promiseCacheState.promise) { + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw promiseCacheState.promise + } } - const newPromiseCache: PromiseCacheState = { - key, + promiseKey, hashedKey, - promise: fn({ key }) + promise: promiseFn({ promiseKey }) .then((data) => { newPromiseCache.data = data }) @@ -85,22 +89,22 @@ export class PromiseCache { throw newPromiseCache.promise } - public getData = (key: Key) => this.cache.get(hashKey(key))?.data - public getError = (key: Key) => this.cache.get(hashKey(key))?.error + public getData = (promiseKey: Key) => this.cache.get(hashKey(promiseKey))?.data + public getError = (promiseKey: Key) => this.cache.get(hashKey(promiseKey))?.error - public subscribe(key: Key, syncSubscriber: Sync) { - const hashedKey = hashKey(key) + public subscribe(promiseKey: Key, syncSubscriber: Sync) { + const hashedKey = hashKey(promiseKey) const syncs = this.syncsMap.get(hashedKey) this.syncsMap.set(hashedKey, [...(syncs ?? []), syncSubscriber]) const subscribed = { - unsubscribe: () => this.unsubscribe(key, syncSubscriber), + unsubscribe: () => this.unsubscribe(promiseKey, syncSubscriber), } return subscribed } - public unsubscribe(key: Key, syncSubscriber: Sync) { - const hashedKey = hashKey(key) + public unsubscribe(promiseKey: Key, syncSubscriber: Sync) { + const hashedKey = hashKey(promiseKey) const syncs = this.syncsMap.get(hashedKey) if (syncs) { @@ -111,8 +115,8 @@ export class PromiseCache { } } - private syncSubscribers = (key?: Key) => { - const hashedKey = key ? hashKey(key) : undefined + private syncSubscribers = (promiseKey?: Key) => { + const hashedKey = promiseKey ? hashKey(promiseKey) : undefined return hashedKey ? this.syncsMap.get(hashedKey)?.forEach((sync) => sync()) diff --git a/packages/promise/src/SuspensePromise.spec.tsx b/packages/promise/src/SuspensePromise.spec.tsx index 635012bd7..2f868803d 100644 --- a/packages/promise/src/SuspensePromise.spec.tsx +++ b/packages/promise/src/SuspensePromise.spec.tsx @@ -18,7 +18,7 @@ describe('', () => { render( - Promise.resolve(TEXT) }}> + Promise.resolve(TEXT) }}> {({ data }) => <>{data}} diff --git a/packages/promise/src/promiseOptions.spec.tsx b/packages/promise/src/promiseOptions.spec.tsx index cfad69af7..315bd51ff 100644 --- a/packages/promise/src/promiseOptions.spec.tsx +++ b/packages/promise/src/promiseOptions.spec.tsx @@ -9,7 +9,7 @@ import { useSuspensePromise } from './useSuspensePromise' const key = (id: number) => ['key', id] as const -const options = promiseOptions({ key: key(1), fn: () => Promise.resolve(TEXT) }) +const options = promiseOptions({ promiseKey: key(1), promiseFn: () => Promise.resolve(TEXT) }) describe('promiseOptions', () => { let promiseCache: PromiseCache diff --git a/packages/promise/src/types.ts b/packages/promise/src/types.ts index 00e806416..504e094f4 100644 --- a/packages/promise/src/types.ts +++ b/packages/promise/src/types.ts @@ -6,8 +6,8 @@ export type Key = Tuple * @experimental This is experimental feature. */ export type SuspensePromiseOptions = { - key: TKey - fn: (options: { key: TKey }) => Promise + promiseKey: TKey + promiseFn: (options: { promiseKey: TKey }) => Promise } export type ResolvedData = { diff --git a/packages/promise/src/useSuspensePromise.spec.tsx b/packages/promise/src/useSuspensePromise.spec.tsx index d44e92fc5..a8766e259 100644 --- a/packages/promise/src/useSuspensePromise.spec.tsx +++ b/packages/promise/src/useSuspensePromise.spec.tsx @@ -9,7 +9,7 @@ import { useSuspensePromise } from './useSuspensePromise' const key = (id: number) => ['key', id] as const const SuspensePromiseSuccess = () => { - const resolvedData = useSuspensePromise({ key: key(1), fn: () => sleep(ms('0.1s')).then(() => TEXT) }) + const resolvedData = useSuspensePromise({ promiseKey: key(1), promiseFn: () => sleep(ms('0.1s')).then(() => TEXT) }) return ( <> @@ -21,8 +21,8 @@ const SuspensePromiseSuccess = () => { const SuspensePromiseFailure = () => { const resolvedData = useSuspensePromise({ - key: key(1), - fn: () => sleep(ms('0.1s')).then(() => Promise.reject(new Error(ERROR_MESSAGE))), + promiseKey: key(1), + promiseFn: () => sleep(ms('0.1s')).then(() => Promise.reject(new Error(ERROR_MESSAGE))), }) return <>{resolvedData.data} diff --git a/packages/promise/src/useSuspensePromise.ts b/packages/promise/src/useSuspensePromise.ts index 1d0f88b96..ac3bb46a0 100644 --- a/packages/promise/src/useSuspensePromise.ts +++ b/packages/promise/src/useSuspensePromise.ts @@ -11,17 +11,17 @@ export const useSuspensePromise = ( const promiseCache = usePromiseCache() const syncData = () => promiseCache.suspend(options) const data = useSyncExternalStore( - (sync) => promiseCache.subscribe(options.key, sync).unsubscribe, + (sync) => promiseCache.subscribe(options.promiseKey, sync).unsubscribe, syncData, syncData ) return useMemo( () => ({ - key: options.key, + promiseKey: options.promiseKey, data, - reset: () => promiseCache.reset(options.key), + reset: () => promiseCache.reset(), }), - [data, options.key] + [data, options.promiseKey, promiseCache] ) } From 405a7b14e78b80990a800422169111ce7f16aa7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=9D=EC=A7=84?= Date: Sat, 13 Jul 2024 23:39:13 +0900 Subject: [PATCH 4/8] chore(promise): add changeset Co-authored-by: Jonghyeon Ko <61593290+manudeli@users.noreply.github.com> --- .changeset/twelve-buttons-do.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-buttons-do.md diff --git a/.changeset/twelve-buttons-do.md b/.changeset/twelve-buttons-do.md new file mode 100644 index 000000000..2bca78468 --- /dev/null +++ b/.changeset/twelve-buttons-do.md @@ -0,0 +1,5 @@ +--- +'@suspensive/promise': minor +--- + +feat(promise): add @suspensive/promise package From ea3107e21a5a02d004cb400580a0fa02e3149eff Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Sat, 13 Jul 2024 23:47:12 +0900 Subject: [PATCH 5/8] Delete .changeset/twelve-buttons-do.md --- .changeset/twelve-buttons-do.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/twelve-buttons-do.md diff --git a/.changeset/twelve-buttons-do.md b/.changeset/twelve-buttons-do.md deleted file mode 100644 index 2bca78468..000000000 --- a/.changeset/twelve-buttons-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@suspensive/promise': minor ---- - -feat(promise): add @suspensive/promise package From 8592f10817b398bd299f59b7490a0f85b65ac401 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Sat, 13 Jul 2024 23:48:41 +0900 Subject: [PATCH 6/8] Update packages/promise/src/index.ts --- packages/promise/src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/promise/src/index.ts b/packages/promise/src/index.ts index 8cd7a2c73..b9b727bba 100644 --- a/packages/promise/src/index.ts +++ b/packages/promise/src/index.ts @@ -5,5 +5,3 @@ export { PromiseCache } from './PromiseCache' export { PromiseCacheProvider } from './PromiseCacheProvider' export { usePromiseCache } from './usePromiseCache' -export type { SuspensePromiseProps } from './SuspensePromise' -export type { SuspensePromiseOptions } from './types' From 4896ea8bca04d499f6a4d52f008e349e048761e7 Mon Sep 17 00:00:00 2001 From: Jonghyeon Ko Date: Sat, 13 Jul 2024 23:52:16 +0900 Subject: [PATCH 7/8] Create blue-apricots-love.md --- .changeset/blue-apricots-love.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/blue-apricots-love.md diff --git a/.changeset/blue-apricots-love.md b/.changeset/blue-apricots-love.md new file mode 100644 index 000000000..b49c3b48b --- /dev/null +++ b/.changeset/blue-apricots-love.md @@ -0,0 +1,5 @@ +--- +"@suspensive/promise": minor +--- + +feat(promise): add SuspensePromise, useSuspensePromise, promiseOptions, PromiseCache, PromiseCacheProvider, usePromiseCache From 39babc9a3b38f887e8e7b90db1703d52af8488e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=84=9D=EC=A7=84?= Date: Sat, 13 Jul 2024 23:55:03 +0900 Subject: [PATCH 8/8] fix(promise): fix eslint error --- packages/promise/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/promise/src/index.ts b/packages/promise/src/index.ts index b9b727bba..ac9101ecf 100644 --- a/packages/promise/src/index.ts +++ b/packages/promise/src/index.ts @@ -4,4 +4,3 @@ export { promiseOptions } from './promiseOptions' export { PromiseCache } from './PromiseCache' export { PromiseCacheProvider } from './PromiseCacheProvider' export { usePromiseCache } from './usePromiseCache' -