muya 2.0.0-beta.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +124 -195
  2. package/cjs/index.js +1 -1
  3. package/esm/create-state.js +1 -0
  4. package/esm/create.js +1 -1
  5. package/esm/debug/development-tools.js +1 -1
  6. package/esm/index.js +1 -1
  7. package/esm/scheduler.js +1 -0
  8. package/esm/select.js +1 -0
  9. package/esm/use-value.js +1 -0
  10. package/esm/utils/__tests__/is.test.js +1 -1
  11. package/esm/utils/common.js +1 -1
  12. package/esm/utils/is.js +1 -1
  13. package/package.json +12 -12
  14. package/src/__tests__/bench.test.tsx +3 -108
  15. package/src/__tests__/create.test.tsx +122 -70
  16. package/src/__tests__/scheduler.test.tsx +52 -0
  17. package/src/__tests__/select.test.tsx +127 -0
  18. package/src/__tests__/use-value.test.tsx +78 -0
  19. package/src/create-state.ts +50 -0
  20. package/src/create.ts +42 -73
  21. package/src/debug/development-tools.ts +18 -3
  22. package/src/index.ts +2 -1
  23. package/src/{utils/global-scheduler.ts → scheduler.ts} +9 -3
  24. package/src/select.ts +69 -0
  25. package/src/types.ts +57 -6
  26. package/src/use-value.ts +22 -0
  27. package/src/utils/__tests__/is.test.ts +24 -7
  28. package/src/utils/common.ts +35 -10
  29. package/src/utils/is.ts +5 -8
  30. package/types/create-state.d.ts +12 -0
  31. package/types/create.d.ts +6 -18
  32. package/types/debug/development-tools.d.ts +2 -9
  33. package/types/index.d.ts +2 -1
  34. package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
  35. package/types/select.d.ts +10 -0
  36. package/types/types.d.ts +55 -5
  37. package/types/use-value.d.ts +2 -0
  38. package/types/utils/common.d.ts +6 -5
  39. package/types/utils/is.d.ts +3 -4
  40. package/esm/__tests__/create-async.test.js +0 -1
  41. package/esm/subscriber.js +0 -1
  42. package/esm/use.js +0 -1
  43. package/esm/utils/__tests__/context.test.js +0 -1
  44. package/esm/utils/__tests__/sub-memo.test.js +0 -1
  45. package/esm/utils/create-context.js +0 -1
  46. package/esm/utils/global-scheduler.js +0 -1
  47. package/esm/utils/scheduler.js +0 -1
  48. package/esm/utils/sub-memo.js +0 -1
  49. package/src/__tests__/create-async.test.ts +0 -88
  50. package/src/__tests__/subscriber.test.tsx +0 -89
  51. package/src/__tests__/use-async.test.tsx +0 -45
  52. package/src/__tests__/use.test.tsx +0 -125
  53. package/src/subscriber.ts +0 -165
  54. package/src/use.ts +0 -57
  55. package/src/utils/__tests__/context.test.ts +0 -198
  56. package/src/utils/__tests__/sub-memo.test.ts +0 -13
  57. package/src/utils/create-context.ts +0 -60
  58. package/src/utils/scheduler.ts +0 -59
  59. package/src/utils/sub-memo.ts +0 -49
  60. package/types/subscriber.d.ts +0 -25
  61. package/types/use.d.ts +0 -2
  62. package/types/utils/create-context.d.ts +0 -5
  63. package/types/utils/global-scheduler.d.ts +0 -5
  64. package/types/utils/sub-memo.d.ts +0 -7
@@ -0,0 +1,2 @@
1
+ import { type GetState } from './types';
2
+ export declare function useValue<T, S>(state: GetState<T>, selector?: (stateValue: T) => S): undefined extends S ? T : S;
@@ -3,13 +3,14 @@ export declare enum Abort {
3
3
  Error = "StateAbortError"
4
4
  }
5
5
  export interface CancelablePromise<T> {
6
- promise?: Promise<T>;
6
+ promise: Promise<T>;
7
7
  controller?: AbortController;
8
- resolveInitialPromise?: (value: T) => void;
9
8
  }
10
9
  /**
11
- * Cancelable promise function, return promise and controller
10
+ * Check if the cache value is different from the previous value.
12
11
  */
13
- export declare function cancelablePromise<T>(promise: Promise<T>, previousController?: AbortController): CancelablePromise<T>;
14
- export declare function generateId(): number;
15
12
  export declare function canUpdate<T>(cache: Cache<T>, isEqual?: IsEqual<T>): boolean;
13
+ /**
14
+ * Handle async updates for `create` and `select`
15
+ */
16
+ export declare function handleAsyncUpdate<T>(cache: Cache<T>, emit: () => void, value: T): T;
@@ -1,5 +1,4 @@
1
- import type { SetStateCb, SetValue } from '../types';
2
- import type { State } from '../create';
1
+ import type { SetStateCb, SetValue, State } from '../types';
3
2
  export declare function isPromise<T>(value: unknown): value is Promise<T>;
4
3
  export declare function isFunction<T extends (...args: unknown[]) => unknown>(value: unknown): value is T;
5
4
  export declare function isMap(value: unknown): value is Map<unknown, unknown>;
@@ -8,6 +7,6 @@ export declare function isArray(value: unknown): value is Array<unknown>;
8
7
  export declare function isEqualBase<T>(valueA: T, valueB: T): boolean;
9
8
  export declare function isSetValueFunction<T>(value: SetValue<T>): value is SetStateCb<T>;
10
9
  export declare function isAbortError(value: unknown): value is DOMException;
11
- export declare function isAnyOtherError(value: unknown): value is Error;
10
+ export declare function isError(value: unknown): value is Error;
12
11
  export declare function isUndefined(value: unknown): value is undefined;
13
- export declare function isCreate(value: unknown): value is State<unknown>;
12
+ export declare function isState<T>(value: unknown): value is State<T>;
@@ -1 +0,0 @@
1
- import{create as r}from"../create";import{waitFor as c}from"@testing-library/react";import{longPromise as m}from"./test-utils";import{isPromise as u}from"../utils/is";import{subscriber as n}from"../subscriber";describe("create",()=>{it("should subscribe to context and notified it with parameters",async()=>{const t=r(1),i=r(2);function s(){return t()+i()}async function a(p){return t()+i()+s()+p}let o=0;const e=n(()=>a(10));expect(u(e.emitter.getSnapshot())).toBe(!0),e.listen(async()=>{o++}),expect(o).toBe(0),expect(await e()).toBe(16),t.set(2),await c(async()=>{}),expect(await e()).toBe(18),expect(o).toBe(4)}),it("should async subscribe to context and notified it",async()=>{const t=r(1),i=r(Promise.resolve(2));async function s(){return await m(),t()+await i()}async function a(){return t()+await i()+await s()}let o=0;const e=n(a);e.listen(()=>{o++}),e(),expect(t.emitter.getSize()).toBe(1),expect(i.emitter.getSize()).toBe(1),expect(e.emitter.getSize()).toBe(1),t.set(2),await c(async()=>{expect(await e()).toBe(8),expect(o).toBe(5)}),i.set(3),await c(async()=>{expect(await e()).toBe(10),expect(o).toBe(10)}),expect(t.emitter.getSize()).toBe(1),expect(i.emitter.getSize()).toBe(1),expect(e.emitter.getSize()).toBe(1),e.destroy(),expect(t.emitter.getSize()).toBe(0),expect(i.emitter.getSize()).toBe(0),expect(e.emitter.getSize()).toBe(0)})});
package/esm/subscriber.js DELETED
@@ -1 +0,0 @@
1
- import{cancelablePromise as F,canUpdate as v,generateId as h}from"./utils/common";import{createContext as x}from"./utils/create-context";import{createEmitter as S}from"./utils/create-emitter";import"./debug/development-tools";import{createGlobalScheduler as C}from"./utils/global-scheduler";import{isAbortError as E,isEqualBase as R,isPromise as m,isUndefined as w}from"./utils/is";const d=C(),b=x(void 0);function U(p){const r={};let u=!1;const a=[],i={},s=S(()=>u?r.current:(u=!0,n()),()=>(u=!0,n()));async function l(){v(r,R)&&(i.controller&&i.controller.abort(),d.schedule(c,null))}const c=h(),f=d.add(c,{onFinish(){r.current=n(),s.emit()}}),T={addEmitter(e){const t=e.subscribe(l);a.push(t)},id:c,sub:l};function y(e){const t=F(e,i.controller);return i.controller=t.controller,t.promise?.then(o=>{r.current=o,s.emit()}).catch(o=>{E(o)||(r.current=o,s.emit())}),t.promise}const n=function(){const e=b.run(T,p);if(!m(e))return r.current=e,e;const t=b.wrap(()=>y(e))();m(t)&&t.catch(()=>null);const o=t;return r.current=o,o};return n.emitter=s,n.destroy=function(){for(const e of a)e();s.clear(),f()},n.id=c,n.listen=function(e){return s.subscribe(()=>{const t=r.current;if(w(t))throw new Error("The value is undefined");e(t)})},n.abort=function(){i.controller&&i.controller.abort()},n}export{b as context,U as subscriber};
package/esm/use.js DELETED
@@ -1 +0,0 @@
1
- import{useDebugValue as u,useEffect as s,useRef as m}from"react";import{EMPTY_SELECTOR as a}from"./types";import{isAnyOtherError as c,isPromise as T}from"./utils/is";import{useSyncExternalStore as d}from"react";import{subMemo as p}from"./utils/sub-memo";const f=10,E=3;function R(t){const e=m({renders:0,startTime:performance.now()});s(()=>{e.current.renders++,!(performance.now()-e.current.startTime<f)&&(e.current.renders<E||(e.current.startTime=performance.now(),e.current.renders=0,console.warn(`Function ${t.name.length>0?t.name:t} seems to be not memoized, wrap the function to the useCallback or use global defined functions.`)))},[t])}function b(t,e=a){const n=p(t),o=n.call(),i=o.emitter.getInitialSnapshot??o.emitter.getSnapshot;s(()=>n.destroy,[t,n.destroy]);const r=d(o.emitter.subscribe,()=>e(o.emitter.getSnapshot()),()=>e(i()));if(u(r),T(r))throw r;if(c(r))throw n.destroy(),r;return r}export{b as use};
@@ -1 +0,0 @@
1
- import{createContext as c}from"../create-context";import{longPromise as r}from"../../__tests__/test-utils";describe("context",()=>{it("should check context",()=>{const e=c({name:"John Doe"}),t=()=>{e.run({name:"Jane Doe"},()=>{expect(e.use()).toEqual({name:"Jane Doe"})})};expect(e.use()).toEqual({name:"John Doe"}),t(),expect(e.use()).toEqual({name:"John Doe"})}),it("should test async context",e=>{const t=c("empty"),o=async()=>new Promise(u=>setTimeout(u,10));t.run("outer",()=>{expect(t.use()).toEqual("outer"),setTimeout(t.wrap(async()=>{try{await o(),expect(t.use()).toEqual("outer"),s()}catch(u){e(u)}}),10),t.run("inner",()=>{expect(t.use()).toEqual("inner"),setTimeout(t.wrap(()=>{try{expect(t.use()).toEqual("inner"),s()}catch(u){e(u)}}),10)}),expect(t.use()).toEqual("outer")});let n=0;function s(){n+=1,n===2&&e()}}),it("should test async nested context",e=>{const t=c(0);t.run(1,()=>{expect(t.use()).toEqual(1),t.run(2,()=>{t.run(3,()=>{expect(t.use()).toEqual(3),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(3)}),10),expect(t.use()).toEqual(3)}),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(2)}),10),expect(t.use()).toEqual(2),t.run(3,()=>{expect(t.use()).toEqual(3),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(3),t.run(4,()=>{expect(t.use()).toEqual(4),setTimeout(t.wrap(()=>{expect(t.use()).toEqual(4),e()}),10),expect(t.use()).toEqual(4)})}),10),expect(t.use()).toEqual(3)}),expect(t.use()).toEqual(2)}),expect(t.use()).toEqual(1)}),expect(t.use()).toEqual(0)}),it("should stress test context with async random code",async()=>{const t=c(0);for(let n=0;n<1e4;n++)t.run(n,()=>{expect(t.use()).toEqual(n)});const o=[];for(let n=0;n<1e4;n++)t.run(n,()=>{expect(t.use()).toEqual(n);const s=new Promise(u=>{setTimeout(t.wrap(()=>{expect(t.use()).toEqual(n),u(n)}),Math.random()*100)});o.push(s)});await Promise.all(o)}),it("should-test-default-value-with-ctx",async()=>{const e=c({counter:1});e.run({counter:10},async()=>{await r(10),expect(e.use()?.counter).toBe(10)}),e.run({counter:12},()=>{expect(e.use()?.counter).toBe(12)})}),it("should test nested context",()=>{const t=c({count:0});function o(){const n=t.use();expect(n?.count).toBe(0),t.run({count:1},()=>{const s=t.use();expect(s?.count).toBe(1),t.run({count:2},()=>{const u=t.use();expect(u?.count).toBe(2)})})}o(),o()}),it("should test nested context with async when promise is returned, but not waited",async()=>{const e=c({count:0});async function t(){await r(10);const n=e.use();expect(n?.count).toBe(1)}async function o(){await t(),e.wrap(t);const n=e.use();expect(n?.count).toBe(1)}e.run({count:1},o)})});
@@ -1 +0,0 @@
1
- import{subMemo as t}from"../sub-memo";describe("memo-fn",()=>{it("should create memo fn",()=>{function e(){return!0}const o=t(e);expect(o.call().emitter).toBeDefined()})});
@@ -1 +0,0 @@
1
- import{isPromise as i}from"./is";const a=Symbol("_");function f(s){const t=[];function o(){if(t.length===0)return s;const e=t.at(-1);return e===a?s:e}function u(e,n){t.push(e);const r=n();return i(r)?(async()=>{try{return await r}finally{t.pop()}})():(t.pop(),r)}function c(e){const n=o();return()=>{t.push(n);const r=e();return i(r)?(async()=>{try{return await r}finally{t.pop()}})():(t.pop(),r)}}return{run:u,use:o,wrap:c}}export{f as createContext};
@@ -1 +0,0 @@
1
- import{RESCHEDULE_COUNT as l,THRESHOLD as f,THRESHOLD_ITEMS as d}from"./scheduler";function m(){const t=new Map,r=new Set;let s=performance.now(),u=!1;function c(){const n=performance.now(),e=n-s,{size:o}=r;if(e<f&&o>0&&o<d){s=n,i();return}u||(u=!0,Promise.resolve().then(()=>{u=!1,s=performance.now(),i()}))}function i(){if(r.size===0)return;const n=new Set;for(const e of r){if(t.has(e.id)){n.add(e.id);const{onResolveItem:o}=t.get(e.id);o&&o(e.value)}r.delete(e)}if(r.size>l){c();return}for(const e of n)t.get(e)?.onFinish()}return{add(n,e){return t.set(n,e),()=>{t.delete(n)}},schedule(n,e){r.add({value:e,id:n}),c()}}}export{m as createGlobalScheduler};
@@ -1 +0,0 @@
1
- const d=.2,T=10,S=0;function E(f){const n=new Set,{onResolveItem:r,onFinish:a}=f;let o=performance.now(),t=!1;function s(){const e=performance.now(),u=e-o,{size:c}=n;if(u<.2&&c>0&&c<10){o=e,i();return}t||(t=!0,Promise.resolve().then(()=>{t=!1,o=performance.now(),i()}))}function i(){if(n.size!==0){for(const e of n)r&&r(e),n.delete(e);if(n.size>0){s();return}a()}}function l(e){n.add(e),s()}return l}export{S as RESCHEDULE_COUNT,d as THRESHOLD,T as THRESHOLD_ITEMS,E as createScheduler};
@@ -1 +0,0 @@
1
- import{subscriber as c}from"../subscriber";const n=new WeakMap;let r=0;function p(){return r}function o(){r++}function b(t){return r=0,{call(){const e=n.get(t);if(e)return e.count++,e.returnType;o();const u={count:1,returnType:c(t)};return n.set(t,u),u.returnType},destroy(){const e=n.get(t);e&&(e.count--,e.count===0&&(e.returnType.destroy(),n.delete(t)))}}}export{p as getDebugCacheCreation,b as subMemo};
@@ -1,88 +0,0 @@
1
- import { create } from '../create'
2
- import { waitFor } from '@testing-library/react'
3
- import { longPromise } from './test-utils'
4
- import { isPromise } from '../utils/is'
5
- import { subscriber } from '../subscriber'
6
-
7
- describe('create', () => {
8
- it('should subscribe to context and notified it with parameters', async () => {
9
- const state1 = create(1)
10
- const state2 = create(2)
11
-
12
- function derivedNested() {
13
- return state1() + state2()
14
- }
15
- async function derived(plus: number) {
16
- return state1() + state2() + derivedNested() + plus
17
- }
18
-
19
- let updatesCounter = 0
20
- const sub = subscriber(() => derived(10))
21
- expect(isPromise(sub.emitter.getSnapshot())).toBe(true)
22
- sub.listen(async () => {
23
- updatesCounter++
24
- })
25
- expect(updatesCounter).toBe(0)
26
- // check if there is not maximum call stack
27
- expect(await sub()).toBe(16)
28
-
29
- state1.set(2)
30
- //
31
- await waitFor(async () => {})
32
- expect(await sub()).toBe(18)
33
-
34
- expect(updatesCounter).toBe(4)
35
- })
36
-
37
- it('should async subscribe to context and notified it', async () => {
38
- const state1 = create(1)
39
- const state2 = create(Promise.resolve(2))
40
-
41
- async function derivedNested() {
42
- await longPromise()
43
- return state1() + (await state2())
44
- }
45
- async function derived() {
46
- return state1() + (await state2()) + (await derivedNested())
47
- }
48
-
49
- let updatesCounter = 0
50
-
51
- const sub = subscriber(derived)
52
-
53
- sub.listen(() => {
54
- updatesCounter++
55
- })
56
-
57
- // check if there is not maximum call stack
58
- sub()
59
-
60
- // check if not assigned multiple times, but only once
61
- expect(state1.emitter.getSize()).toBe(1)
62
- expect(state2.emitter.getSize()).toBe(1)
63
- expect(sub.emitter.getSize()).toBe(1)
64
- state1.set(2)
65
-
66
- await waitFor(async () => {
67
- expect(await sub()).toBe(8)
68
- expect(updatesCounter).toBe(5)
69
- })
70
-
71
- state2.set(3)
72
-
73
- await waitFor(async () => {
74
- expect(await sub()).toBe(10)
75
- expect(updatesCounter).toBe(10)
76
- })
77
-
78
- expect(state1.emitter.getSize()).toBe(1)
79
- expect(state2.emitter.getSize()).toBe(1)
80
- expect(sub.emitter.getSize()).toBe(1)
81
-
82
- sub.destroy()
83
-
84
- expect(state1.emitter.getSize()).toBe(0)
85
- expect(state2.emitter.getSize()).toBe(0)
86
- expect(sub.emitter.getSize()).toBe(0)
87
- })
88
- })
@@ -1,89 +0,0 @@
1
- import { waitFor } from '@testing-library/react'
2
- import { create } from '../create'
3
- import { subscriber } from '../subscriber'
4
- import { longPromise } from './test-utils'
5
-
6
- describe('subscriber', () => {
7
- it('should test subscriber and cleaning the emitters', () => {
8
- const state1 = create(1)
9
- const sub = subscriber(state1)
10
- // at this point, the emitter is not subscribed yet, as it need to be called first.
11
- expect(state1.emitter.getSize()).toBe(0)
12
-
13
- // check if the value is correct
14
- expect(sub()).toBe(1)
15
-
16
- // now we can check if the value is subscribed
17
- expect(state1.emitter.getSize()).toBe(1)
18
- // we destroy the subscriber, meaning that the emitter should be cleaned
19
-
20
- sub.destroy()
21
-
22
- expect(state1.emitter.getSize()).toBe(0)
23
-
24
- // and test re-aligning the subscriber
25
- expect(sub()).toBe(1)
26
- expect(state1.emitter.getSize()).toBe(1)
27
- })
28
- it('should test how many events are emitter via singleton state', async () => {
29
- const state1 = create(1)
30
- const sub = subscriber(state1)
31
-
32
- let updateCount = 0
33
-
34
- sub.listen(() => {
35
- updateCount++
36
- })
37
- sub()
38
- await waitFor(() => {})
39
- // we do not received initial value as it is not changed
40
- expect(updateCount).toBe(0)
41
-
42
- state1.set(2)
43
- await waitFor(() => {})
44
- expect(updateCount).toBe(1)
45
- })
46
- it('should test how many events are emitter via singleton async state', async () => {
47
- const state1 = create(longPromise())
48
- const sub = subscriber(state1)
49
-
50
- let updateCount = 0
51
-
52
- sub.listen(() => {
53
- updateCount++
54
- })
55
- sub()
56
- await waitFor(() => {})
57
- // we do not received initial value as it is not changed
58
- expect(updateCount).toBe(0)
59
-
60
- state1.set(2)
61
- await waitFor(() => {})
62
- expect(updateCount).toBe(1)
63
- })
64
-
65
- it('should test how many events are emitter via derived state', async () => {
66
- const state1 = create(longPromise())
67
-
68
- async function derived() {
69
- // await longPromise()
70
- return await state1()
71
- }
72
- const sub = subscriber(derived)
73
-
74
- let updateCount = 0
75
-
76
- sub.listen(() => {
77
- updateCount++
78
- })
79
- await sub()
80
- await waitFor(() => {})
81
- expect(await sub()).toBe(0)
82
- // // we do not received initial value as it is not changed
83
- expect(updateCount).toBe(2)
84
-
85
- // state1.set(2)
86
- // await waitFor(() => {})
87
- // expect(updateCount).toBe(1)
88
- })
89
- })
@@ -1,45 +0,0 @@
1
- /* eslint-disable @typescript-eslint/ban-ts-comment */
2
- import { renderHook } from '@testing-library/react-hooks'
3
- import { create } from '../create'
4
- import { use } from '../use'
5
- import { Suspense } from 'react'
6
- import { waitFor } from '@testing-library/react'
7
-
8
- describe('use-create', () => {
9
- const reRendersBefore = jest.fn()
10
-
11
- beforeEach(() => {
12
- jest.clearAllMocks()
13
- })
14
-
15
- it('should test sub hook', async () => {
16
- const userState = create({ name: 'John', age: 30 })
17
- async function getDataWithUser() {
18
- const result = await fetch('https://jsonplaceholder.typicode.com/todos/1')
19
- const json = await result.json()
20
- return { age: userState().age, ...json }
21
- }
22
-
23
- const suspenseFunction = jest.fn()
24
- function Loading() {
25
- suspenseFunction()
26
- return <div>Loading...</div>
27
- }
28
-
29
- const { result } = renderHook(
30
- () => {
31
- reRendersBefore()
32
- const data = use(getDataWithUser)
33
- return data
34
- },
35
- // @ts-expect-error
36
- { wrapper: ({ children }) => <Suspense fallback={<Loading />}>{children}</Suspense> },
37
- )
38
-
39
- await waitFor(() => {
40
- expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
41
- })
42
- expect(suspenseFunction).toHaveBeenCalledTimes(1)
43
- expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
44
- })
45
- })
@@ -1,125 +0,0 @@
1
- import { act, renderHook } from '@testing-library/react-hooks'
2
- import { create } from '../create'
3
- import { use } from '../use'
4
- import { waitFor } from '@testing-library/react'
5
- import { useCallback } from 'react'
6
- import { getDebugCacheCreation } from '../utils/sub-memo'
7
-
8
- describe('use-create', () => {
9
- const reRendersBefore = jest.fn()
10
-
11
- beforeEach(() => {
12
- jest.clearAllMocks()
13
- })
14
-
15
- it('should test use hook', async () => {
16
- const state = create(1)
17
-
18
- const { result } = renderHook(() => {
19
- reRendersBefore()
20
- return use(state)
21
- })
22
-
23
- state.set(2)
24
-
25
- await waitFor(() => {})
26
- expect(result.current).toBe(2)
27
- expect(reRendersBefore).toHaveBeenCalledTimes(2)
28
-
29
- state.set(3)
30
-
31
- await waitFor(() => {})
32
- expect(result.current).toBe(3)
33
- expect(reRendersBefore).toHaveBeenCalledTimes(3)
34
- })
35
-
36
- it('should test derived state with multiple states', async () => {
37
- const state1 = create(1)
38
- const state2 = create(2)
39
-
40
- function derivedBefore(plusValue: number) {
41
- return state1() + state2() + plusValue
42
- }
43
-
44
- function derived() {
45
- return state1() + state2() + derivedBefore(10)
46
- }
47
-
48
- const { result } = renderHook(() => {
49
- reRendersBefore()
50
- return use(derived)
51
- })
52
-
53
- await waitFor(() => {})
54
- expect(reRendersBefore).toHaveBeenCalledTimes(1)
55
- act(() => {
56
- state1.set(2)
57
- state2.set(3)
58
- })
59
-
60
- await waitFor(() => {})
61
- expect(result.current).toBe(20)
62
- expect(reRendersBefore).toHaveBeenCalledTimes(2)
63
- })
64
-
65
- it('should test use hook without memoize fn', async () => {
66
- const state1 = create(1)
67
- const state2 = create(2)
68
-
69
- function derivedBefore(plusValue: number) {
70
- return state1() + state2() + plusValue
71
- }
72
-
73
- function derived(add: number) {
74
- return state1() + state2() + derivedBefore(add)
75
- }
76
-
77
- const { result } = renderHook(() => {
78
- reRendersBefore()
79
- return use(() => derived(10))
80
- })
81
- expect(getDebugCacheCreation()).toBe(1)
82
-
83
- await waitFor(() => {})
84
- expect(reRendersBefore).toHaveBeenCalledTimes(1)
85
- act(() => {
86
- state1.set(2)
87
- state2.set(3)
88
- })
89
- expect(getDebugCacheCreation()).toBe(1)
90
- await waitFor(() => {})
91
- expect(result.current).toBe(20)
92
- expect(reRendersBefore).toHaveBeenCalledTimes(2)
93
- })
94
-
95
- it('should test use hook with memoize fn', async () => {
96
- const state1 = create(1)
97
- const state2 = create(2)
98
-
99
- function derivedBefore(plusValue: number) {
100
- return state1() + state2() + plusValue
101
- }
102
-
103
- function derived(add: number) {
104
- return state1() + state2() + derivedBefore(add)
105
- }
106
-
107
- const { result } = renderHook(() => {
108
- reRendersBefore()
109
- const memoized = useCallback(() => derived(10), [])
110
- return use(memoized)
111
- })
112
- expect(getDebugCacheCreation()).toBe(1)
113
-
114
- await waitFor(() => {})
115
- expect(reRendersBefore).toHaveBeenCalledTimes(1)
116
- act(() => {
117
- state1.set(2)
118
- state2.set(3)
119
- })
120
- expect(getDebugCacheCreation()).toBe(1)
121
- await waitFor(() => {})
122
- expect(result.current).toBe(20)
123
- expect(reRendersBefore).toHaveBeenCalledTimes(2)
124
- })
125
- })
package/src/subscriber.ts DELETED
@@ -1,165 +0,0 @@
1
- import type { AnyFunction, Cache, Callable, Listener } from './types'
2
- import type { CancelablePromise } from './utils/common'
3
- import { cancelablePromise, canUpdate, generateId } from './utils/common'
4
- import { createContext } from './utils/create-context'
5
- import type { Emitter } from './utils/create-emitter'
6
- import { createEmitter } from './utils/create-emitter'
7
- import type { StateType } from './debug/development-tools'
8
- import { developmentToolsListener, sendToDevelopmentTools } from './debug/development-tools'
9
- import { createGlobalScheduler } from './utils/global-scheduler'
10
- import { isAbortError, isCreate, isEqualBase, isPromise, isUndefined } from './utils/is'
11
-
12
- const subscriberScheduler = createGlobalScheduler()
13
- interface SubscribeContext<T = unknown> {
14
- addEmitter: (emitter: Emitter<T>) => void
15
- id: number
16
- sub: () => void
17
- }
18
- interface SubscribeRaw<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> {
19
- (): T
20
- emitter: Emitter<T | undefined>
21
- destroy: () => void
22
- id: number
23
- listen: Listener<T>
24
- abort: () => void
25
- }
26
-
27
- export type Subscribe<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>> = {
28
- readonly [K in keyof SubscribeRaw<F, T>]: SubscribeRaw<F, T>[K]
29
- } & Callable<T>
30
-
31
- export const context = createContext<SubscribeContext | undefined>(undefined)
32
-
33
- export function subscriber<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>>(
34
- anyFunction: () => T,
35
- ): Subscribe<F, T> {
36
- const cache: Cache<T> = {}
37
- let isInitialized = false
38
- const cleaners: Array<() => void> = []
39
- const promiseData: CancelablePromise<T> = {}
40
-
41
- const emitter = createEmitter(
42
- () => {
43
- if (!isInitialized) {
44
- isInitialized = true
45
- return subscribe()
46
- }
47
- return cache.current
48
- },
49
- () => {
50
- isInitialized = true
51
- return subscribe()
52
- },
53
- )
54
-
55
- async function sub() {
56
- if (!canUpdate(cache, isEqualBase)) {
57
- return
58
- }
59
- if (promiseData.controller) {
60
- promiseData.controller.abort()
61
- }
62
-
63
- subscriberScheduler.schedule(id, null)
64
- }
65
-
66
- const id = generateId()
67
-
68
- const clearScheduler = subscriberScheduler.add(id, {
69
- onFinish() {
70
- cache.current = subscribe()
71
- emitter.emit()
72
- },
73
- })
74
- const ctx: SubscribeContext = {
75
- addEmitter(stateEmitter) {
76
- const clean = stateEmitter.subscribe(sub)
77
- cleaners.push(clean)
78
- },
79
- id,
80
- sub,
81
- }
82
-
83
- function asyncSub(resultValue: Promise<T>): Promise<T> | undefined {
84
- const cancel = cancelablePromise<T>(resultValue, promiseData.controller)
85
- promiseData.controller = cancel.controller
86
- cancel.promise
87
- ?.then((value) => {
88
- cache.current = value
89
- emitter.emit()
90
- })
91
- .catch((error) => {
92
- if (isAbortError(error)) {
93
- return
94
- }
95
- cache.current = error
96
- emitter.emit()
97
- })
98
- return cancel.promise
99
- }
100
-
101
- const subscribe = function (): T {
102
- const resultValue = context.run(ctx, anyFunction)
103
-
104
- if (!isPromise(resultValue)) {
105
- cache.current = resultValue
106
- return resultValue
107
- }
108
-
109
- const promise = context.wrap(() => asyncSub(resultValue))()
110
- if (isPromise(promise)) {
111
- // we do not do anything with the promise, because it is already handled in asyncSub
112
- promise.catch(() => null)
113
- }
114
- const promiseAsT = promise as T
115
- cache.current = promiseAsT
116
- return promiseAsT
117
- }
118
-
119
- subscribe.emitter = emitter
120
- subscribe.destroy = function () {
121
- for (const cleaner of cleaners) {
122
- cleaner()
123
- }
124
- emitter.clear()
125
- clearScheduler()
126
- }
127
- subscribe.id = id
128
- subscribe.listen = function (listener: (value: T) => void) {
129
- return emitter.subscribe(() => {
130
- const final = cache.current
131
- if (isUndefined(final)) {
132
- throw new Error('The value is undefined')
133
- }
134
- listener(final)
135
- })
136
- }
137
-
138
- if (process.env.NODE_ENV === 'development') {
139
- let name: string | undefined
140
- let type: StateType = 'derived'
141
- if (isCreate(anyFunction)) {
142
- type = 'state'
143
- name = anyFunction.stateName
144
- }
145
-
146
- if (!name) {
147
- name = anyFunction.name.length > 0 ? anyFunction.name : anyFunction.toString()
148
- }
149
-
150
- sendToDevelopmentTools({
151
- name,
152
- type,
153
- value: subscribe(),
154
- message: 'init',
155
- })
156
- const listener = developmentToolsListener(name, type)
157
- subscribe.listen(listener)
158
- }
159
- subscribe.abort = function () {
160
- if (promiseData.controller) {
161
- promiseData.controller.abort()
162
- }
163
- }
164
- return subscribe
165
- }
package/src/use.ts DELETED
@@ -1,57 +0,0 @@
1
- /* eslint-disable react-hooks/rules-of-hooks */
2
- /* eslint-disable sonarjs/rules-of-hooks */
3
- import { useDebugValue, useEffect, useRef } from 'react'
4
- import { EMPTY_SELECTOR, type AnyFunction } from './types'
5
- import { isAnyOtherError, isPromise } from './utils/is'
6
- import { useSyncExternalStore } from 'react'
7
- import { subMemo } from './utils/sub-memo'
8
-
9
- const PROMOTE_DEBUG_AFTER_REACH_TIMES = 10
10
- const PROMOTE_DEBUG_AFTER_REACH_COUNT = 3
11
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
- function useDebugFunction<F extends AnyFunction>(function_: F) {
13
- const renderCount = useRef({ renders: 0, startTime: performance.now() })
14
- useEffect(() => {
15
- renderCount.current.renders++
16
- const passedTime = performance.now() - renderCount.current.startTime
17
- if (passedTime < PROMOTE_DEBUG_AFTER_REACH_TIMES) {
18
- return
19
- }
20
- if (renderCount.current.renders < PROMOTE_DEBUG_AFTER_REACH_COUNT) {
21
- return
22
- }
23
- renderCount.current.startTime = performance.now()
24
- renderCount.current.renders = 0
25
- // eslint-disable-next-line no-console
26
- console.warn(
27
- `Function ${function_.name.length > 0 ? function_.name : function_} seems to be not memoized, wrap the function to the useCallback or use global defined functions.`,
28
- )
29
- }, [function_])
30
- }
31
-
32
- export function use<F extends AnyFunction, T extends ReturnType<F>, S extends ReturnType<F>>(
33
- anyFunction: () => T,
34
- selector: (stateValue: T) => S = EMPTY_SELECTOR,
35
- ): undefined extends S ? T : S {
36
- const memo = subMemo(anyFunction)
37
- const sub = memo.call()
38
- const initialSnapshot = sub.emitter.getInitialSnapshot ?? sub.emitter.getSnapshot
39
- useEffect(() => {
40
- return memo.destroy
41
- }, [anyFunction, memo.destroy])
42
-
43
- const value = useSyncExternalStore<S>(
44
- sub.emitter.subscribe,
45
- () => selector(sub.emitter.getSnapshot() as T),
46
- () => selector(initialSnapshot() as T),
47
- )
48
- useDebugValue(value)
49
- if (isPromise(value)) {
50
- throw value
51
- }
52
- if (isAnyOtherError(value)) {
53
- memo.destroy()
54
- throw value
55
- }
56
- return value
57
- }