Skip to content

Commit

Permalink
Merge pull request #3 from samuelgja/feat/add-parameter
Browse files Browse the repository at this point in the history
chore: fix major issue update readme and pick
  • Loading branch information
samuelgja authored Nov 22, 2024
2 parents bd76494 + 3146812 commit 58c679d
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 8 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "muya",
"version": "2.0.1",
"version": "2.0.2",
"author": "[email protected]",
"repository": "https://github.com/samuelgjabel/muya",
"main": "cjs/index.js",
Expand Down
68 changes: 67 additions & 1 deletion packages/core/__tests__/create.test.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -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)
})
Expand Down Expand Up @@ -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)
})
})
})
28 changes: 24 additions & 4 deletions packages/core/create.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -19,21 +19,37 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = 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<T>, value: SetStateCb<T>) {
await previousPromise
const newValue = value(cache.current as Awaited<T>)
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
cache.current = resolvedValue
}

function setValue(value: SetValue<T>) {
const previous = getValue()
const isFunctionValue = isSetValueFunction(value)

if (isFunctionValue && isPromise(previous)) {
handleAsyncSetValue(previous as Promise<T>, 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<T>) : value
const resolvedValue = handleAsyncUpdate(cache, state.emitter.emit, newValue)
cache.current = resolvedValue
}
Expand Down Expand Up @@ -62,6 +78,10 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
onResolveItem: setValue,
})

if (!isFunction(initialValue)) {
getValue()
}

subscribeToDevelopmentTools(state)
return state
}
1 change: 0 additions & 1 deletion packages/core/debug/development-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,5 @@ export function subscribeToDevelopmentTools<T>(state: State<T> | GetState<T>) {
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))
}
2 changes: 1 addition & 1 deletion packages/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Emitter } from './utils/create-emitter'

export type IsEqual<T = unknown> = (a: T, b: T) => boolean
export type SetStateCb<T> = (value: T | Awaited<T>) => Awaited<T>
export type SetStateCb<T> = (value: Awaited<T>) => Awaited<T>
export type SetValue<T> = SetStateCb<T> | Awaited<T>
export type DefaultValue<T> = T | (() => T)
export type Listener<T> = (listener: (value: T) => void) => () => void
Expand Down

0 comments on commit 58c679d

Please sign in to comment.