From 3146812609d8b9e74b054b7c3ca594654b4dcdf4 Mon Sep 17 00:00:00 2001 From: "samuel.gjabel" Date: Fri, 22 Nov 2024 18:23:31 +0700 Subject: [PATCH] chore: fix major issue update readme and pick --- README.md | 34 ++++++++++++ package.json | 2 +- packages/core/__tests__/create.test.tsx | 68 +++++++++++++++++++++++- packages/core/create.ts | 28 ++++++++-- packages/core/debug/development-tools.ts | 1 - packages/core/types.ts | 2 +- 6 files changed, 127 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 138a366..63e411f 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,40 @@ const asyncState = state.select(async (s) => { ``` --- +### Lazy resolution +`Muya` can be used in `immediate` mode or in `lazy` mode. When create a state with just plain data, it will be in immediate mode, but if you create a state with a function, it will be in lazy mode. This is useful when you want to create a state that is executed only when it is accessed for the first time. + + +```typescript +// immediate mode, so no matter what, this value is already stored in memory +const state = create(0) + +// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render +const state = create(() => 0) +``` + +And in async: +```typescript +// we can create some initial functions like this +async function initialLoad() { + return 0 +} +// immediate mode, so no matter what, this value is already stored in memory +const state = create(initialLoad) +// or +const state = create(Promise.resolve(0)) + +// lazy mode, value is not stored in memory until it is accessed for the first time via get or component render +const state = create(() => Promise.resolve(0)) +``` + +And when setting state when initial value is promise, set is always sync. +But as in react there are two methods how to set a state. Directly `.set(2)` or with a function `.set((prev) => prev + 1)`. + +So how `set` state will behave with async initial value? +1. Directly call `.set(2)` will be sync, and will set the value to 2 (it will cancel the initial promise) +2. Call `.set((prev) => prev + 1)` will wait until previous promise is resolved, so previous value in set callback is always resolved. + ### Debugging `Muya` in dev mode automatically connects to the `redux` devtools extension if it is installed in the browser. For now devtool api is simple - state updates. diff --git a/package.json b/package.json index a2988f2..d0422d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "muya", - "version": "2.0.1", + "version": "2.0.2", "author": "samuel.gjabel@gmail.com", "repository": "https://github.com/samuelgjabel/muya", "main": "cjs/index.js", diff --git a/packages/core/__tests__/create.test.tsx b/packages/core/__tests__/create.test.tsx index 472e856..65d29b9 100644 --- a/packages/core/__tests__/create.test.tsx +++ b/packages/core/__tests__/create.test.tsx @@ -1,6 +1,7 @@ import { create } from '../create' import { waitFor } from '@testing-library/react' import { longPromise } from './test-utils' +import { isPromise } from '../utils/is' describe('create', () => { it('should get basic value states', async () => { @@ -56,9 +57,16 @@ describe('create', () => { }) }) - it('should initialize state with a function', () => { + it('should initialize state with a lazy value', () => { const initialValue = jest.fn(() => 10) const state = create(initialValue) + expect(initialValue).not.toHaveBeenCalled() + expect(state.get()).toBe(10) + }) + + it('should initialize state with direct lazy value', () => { + const initialValue = jest.fn(() => 10) + const state = create(initialValue()) expect(initialValue).toHaveBeenCalled() expect(state.get()).toBe(10) }) @@ -156,4 +164,62 @@ describe('create', () => { expect(listener).toHaveBeenCalledWith(2) }) }) + + it('should resolve immediately when state is promise', async () => { + const promiseMock = jest.fn(() => longPromise(100)) + const state1 = create(promiseMock()) + expect(promiseMock).toHaveBeenCalled() + state1.set((value) => { + // set with callback will be executed later when promise is resolved + expect(isPromise(value)).toBe(false) + return value + 1 + }) + + await waitFor(() => { + expect(state1.get()).toBe(1) + }) + + state1.set(2) + await waitFor(() => { + expect(state1.get()).toBe(2) + }) + + state1.set((value) => { + expect(isPromise(value)).toBe(false) + return value + 1 + }) + + await waitFor(() => { + expect(state1.get()).toBe(3) + }) + }) + + it('should resolve lazy when state is promise', async () => { + const promiseMock = jest.fn(() => longPromise(100)) + const state1 = create(promiseMock) + expect(promiseMock).not.toHaveBeenCalled() + state1.set((value) => { + // set with callback will be executed later when promise is resolved + expect(isPromise(value)).toBe(false) + return value + 1 + }) + + await waitFor(() => { + expect(state1.get()).toBe(1) + }) + + state1.set(2) + await waitFor(() => { + expect(state1.get()).toBe(2) + }) + + state1.set((value) => { + expect(isPromise(value)).toBe(false) + return value + 1 + }) + + await waitFor(() => { + expect(state1.get()).toBe(3) + }) + }) }) diff --git a/packages/core/create.ts b/packages/core/create.ts index 3ebfe7c..b0f7a5d 100644 --- a/packages/core/create.ts +++ b/packages/core/create.ts @@ -1,6 +1,6 @@ import { canUpdate, handleAsyncUpdate } from './utils/common' -import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is' -import type { Cache, DefaultValue, IsEqual, SetValue, State } from './types' +import { isEqualBase, isFunction, isPromise, isSetValueFunction, isUndefined } from './utils/is' +import type { Cache, DefaultValue, IsEqual, SetStateCb, SetValue, State } from './types' import { createScheduler } from './scheduler' import { subscribeToDevelopmentTools } from './debug/development-tools' import { createState } from './create-state' @@ -19,21 +19,37 @@ export function create(initialValue: DefaultValue, isEqual: IsEqual = i const value = isFunction(initialValue) ? initialValue() : initialValue const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, value) cache.current = resolvedValue + + return cache.current } return cache.current } catch (error) { cache.current = error as T } + return cache.current } + async function handleAsyncSetValue(previousPromise: Promise, value: SetStateCb) { + await previousPromise + const newValue = value(cache.current as Awaited) + const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue) + cache.current = resolvedValue + } + function setValue(value: SetValue) { + const previous = getValue() + const isFunctionValue = isSetValueFunction(value) + + if (isFunctionValue && isPromise(previous)) { + handleAsyncSetValue(previous as Promise, value) + return + } if (cache.abortController) { cache.abortController.abort() } - const previous = getValue() - const newValue = isSetValueFunction(value) ? value(previous) : value + const newValue = isFunctionValue ? value(previous as Awaited) : value const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue) cache.current = resolvedValue } @@ -62,6 +78,10 @@ export function create(initialValue: DefaultValue, isEqual: IsEqual = i onResolveItem: setValue, }) + if (!isFunction(initialValue)) { + getValue() + } + subscribeToDevelopmentTools(state) return state } diff --git a/packages/core/debug/development-tools.ts b/packages/core/debug/development-tools.ts index 103e5cf..63dc65e 100644 --- a/packages/core/debug/development-tools.ts +++ b/packages/core/debug/development-tools.ts @@ -47,6 +47,5 @@ export function subscribeToDevelopmentTools(state: State | GetState) { type = 'derived' } const name = state.stateName?.length ? state.stateName : `${type}(${state.id.toString()})` - sendToDevelopmentTools({ name, type, value: state.get(), message: 'initial' }) return state.listen(developmentToolsListener(name, type)) } diff --git a/packages/core/types.ts b/packages/core/types.ts index bbb2c57..1bb4bf9 100644 --- a/packages/core/types.ts +++ b/packages/core/types.ts @@ -1,7 +1,7 @@ import type { Emitter } from './utils/create-emitter' export type IsEqual = (a: T, b: T) => boolean -export type SetStateCb = (value: T | Awaited) => Awaited +export type SetStateCb = (value: Awaited) => Awaited export type SetValue = SetStateCb | Awaited export type DefaultValue = T | (() => T) export type Listener = (listener: (value: T) => void) => () => void