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.
- package/README.md +124 -195
- package/cjs/index.js +1 -1
- package/esm/create-state.js +1 -0
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -1
- package/esm/index.js +1 -1
- package/esm/scheduler.js +1 -0
- package/esm/select.js +1 -0
- package/esm/use-value.js +1 -0
- package/esm/utils/__tests__/is.test.js +1 -1
- package/esm/utils/common.js +1 -1
- package/esm/utils/is.js +1 -1
- package/package.json +12 -12
- package/src/__tests__/bench.test.tsx +3 -108
- package/src/__tests__/create.test.tsx +122 -70
- package/src/__tests__/scheduler.test.tsx +52 -0
- package/src/__tests__/select.test.tsx +127 -0
- package/src/__tests__/use-value.test.tsx +78 -0
- package/src/create-state.ts +50 -0
- package/src/create.ts +42 -73
- package/src/debug/development-tools.ts +18 -3
- package/src/index.ts +2 -1
- package/src/{utils/global-scheduler.ts → scheduler.ts} +9 -3
- package/src/select.ts +69 -0
- package/src/types.ts +57 -6
- package/src/use-value.ts +22 -0
- package/src/utils/__tests__/is.test.ts +24 -7
- package/src/utils/common.ts +35 -10
- package/src/utils/is.ts +5 -8
- package/types/create-state.d.ts +12 -0
- package/types/create.d.ts +6 -18
- package/types/debug/development-tools.d.ts +2 -9
- package/types/index.d.ts +2 -1
- package/types/{utils/scheduler.d.ts → scheduler.d.ts} +4 -1
- package/types/select.d.ts +10 -0
- package/types/types.d.ts +55 -5
- package/types/use-value.d.ts +2 -0
- package/types/utils/common.d.ts +6 -5
- package/types/utils/is.d.ts +3 -4
- package/esm/__tests__/create-async.test.js +0 -1
- package/esm/subscriber.js +0 -1
- package/esm/use.js +0 -1
- package/esm/utils/__tests__/context.test.js +0 -1
- package/esm/utils/__tests__/sub-memo.test.js +0 -1
- package/esm/utils/create-context.js +0 -1
- package/esm/utils/global-scheduler.js +0 -1
- package/esm/utils/scheduler.js +0 -1
- package/esm/utils/sub-memo.js +0 -1
- package/src/__tests__/create-async.test.ts +0 -88
- package/src/__tests__/subscriber.test.tsx +0 -89
- package/src/__tests__/use-async.test.tsx +0 -45
- package/src/__tests__/use.test.tsx +0 -125
- package/src/subscriber.ts +0 -165
- package/src/use.ts +0 -57
- package/src/utils/__tests__/context.test.ts +0 -198
- package/src/utils/__tests__/sub-memo.test.ts +0 -13
- package/src/utils/create-context.ts +0 -60
- package/src/utils/scheduler.ts +0 -59
- package/src/utils/sub-memo.ts +0 -49
- package/types/subscriber.d.ts +0 -25
- package/types/use.d.ts +0 -2
- package/types/utils/create-context.d.ts +0 -5
- package/types/utils/global-scheduler.d.ts +0 -5
- package/types/utils/sub-memo.d.ts +0 -7
package/types/utils/common.d.ts
CHANGED
|
@@ -3,13 +3,14 @@ export declare enum Abort {
|
|
|
3
3
|
Error = "StateAbortError"
|
|
4
4
|
}
|
|
5
5
|
export interface CancelablePromise<T> {
|
|
6
|
-
promise
|
|
6
|
+
promise: Promise<T>;
|
|
7
7
|
controller?: AbortController;
|
|
8
|
-
resolveInitialPromise?: (value: T) => void;
|
|
9
8
|
}
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
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;
|
package/types/utils/is.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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};
|
package/esm/utils/scheduler.js
DELETED
|
@@ -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};
|
package/esm/utils/sub-memo.js
DELETED
|
@@ -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
|
-
}
|