muya 2.0.0-beta.1 → 2.0.0-beta.3
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 +3 -3
- package/cjs/index.js +1 -1
- package/esm/create.js +1 -1
- package/esm/debug/development-tools.js +1 -0
- package/esm/subscriber.js +1 -1
- package/esm/use.js +1 -1
- package/esm/utils/global-scheduler.js +1 -0
- package/esm/utils/is.js +1 -1
- package/esm/utils/scheduler.js +1 -1
- package/esm/utils/sub-memo.js +1 -1
- package/package.json +1 -1
- package/{packages/core → src}/__tests__/bench.test.tsx +6 -1
- package/src/__tests__/subscriber.test.tsx +89 -0
- package/{packages/core → src}/__tests__/use-async.test.tsx +5 -4
- package/src/__tests__/use.test.tsx +125 -0
- package/{packages/core → src}/create.ts +43 -12
- package/src/debug/development-tools.ts +37 -0
- package/{packages/core → src}/subscriber.ts +69 -25
- package/{packages/core → src}/use.ts +1 -3
- package/{packages/core → src}/utils/create-emitter.ts +2 -0
- package/src/utils/global-scheduler.ts +75 -0
- package/{packages/core → src}/utils/is.ts +7 -0
- package/{packages/core → src}/utils/scheduler.ts +5 -5
- package/{packages/core → src}/utils/sub-memo.ts +12 -0
- package/types/create.d.ts +7 -0
- package/types/debug/development-tools.d.ts +10 -0
- package/types/utils/global-scheduler.d.ts +5 -0
- package/types/utils/is.d.ts +2 -0
- package/types/utils/scheduler.d.ts +5 -3
- package/types/utils/sub-memo.d.ts +1 -0
- package/packages/core/__tests__/use.test.tsx +0 -76
- /package/{packages/core → src}/__tests__/create-async.test.ts +0 -0
- /package/{packages/core → src}/__tests__/create.test.tsx +0 -0
- /package/{packages/core → src}/__tests__/test-utils.ts +0 -0
- /package/{packages/core → src}/index.ts +0 -0
- /package/{packages/core → src}/types.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/context.test.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/is.test.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/shallow.test.ts +0 -0
- /package/{packages/core → src}/utils/__tests__/sub-memo.test.ts +0 -0
- /package/{packages/core → src}/utils/common.ts +0 -0
- /package/{packages/core → src}/utils/create-context.ts +0 -0
- /package/{packages/core → src}/utils/shallow.ts +0 -0
package/README.md
CHANGED
|
@@ -32,19 +32,19 @@ This allows you to write derived state as plain functions, making your code more
|
|
|
32
32
|
## 📦 Installation
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
npm install muya
|
|
35
|
+
npm install muya@2.0.0-beta.2
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Or using Yarn:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
yarn add muya
|
|
41
|
+
yarn add muya@2.0.0-beta.2
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Or using Bun:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
bun add muya
|
|
47
|
+
bun add muya@2.0.0-beta.2
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
---
|
package/cjs/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var C=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var W=(e,t)=>{for(var n in t)C(e,n,{get:t[n],enumerable:!0})},$=(e,t,n,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of K(t))!Y.call(e,o)&&o!==n&&C(e,o,{get:()=>t[o],enumerable:!(s=X(t,o))||s.enumerable});return e};var J=e=>$(C({},"__esModule",{value:!0}),e);var ee={};W(ee,{EMPTY_SELECTOR:()=>F,create:()=>z,shallow:()=>q,use:()=>j});module.exports=J(ee);var F=e=>e;function p(e){return e instanceof Promise}function k(e){return typeof e=="function"}function P(e){return e instanceof Map}function g(e){return e instanceof Set}function R(e){return Array.isArray(e)}function b(e,t){return e===t?!0:!!Object.is(e,t)}function O(e){return typeof e=="function"}function v(e){return e instanceof DOMException&&e.name==="StateAbortError"}function A(e){return e instanceof Error&&e.name!=="StateAbortError"}function d(e){return e===void 0}function I(e,t){t&&t.abort();let n=new AbortController,{signal:s}=n;return{promise:new Promise((r,u)=>{s.addEventListener("abort",()=>{u(new DOMException("Promise was aborted","StateAbortError"))}),e.then(r).catch(u)}),controller:n}}var Q=0;function y(){return Q++}function S(e,t=(n,s)=>n===s){if(!d(e.current)){if(!d(e.previous)&&t(e.current,e.previous))return!1;e.previous=e.current}return!0}function h(e,t){let n=new Set,s=[];return{clear:()=>{for(let o of s)o();n.clear()},subscribe:o=>(n.add(o),()=>{n.delete(o)}),emit:(...o)=>{for(let r of n)r(...o)},contains:o=>n.has(o),getSnapshot:e,getInitialSnapshot:t,getSize:()=>n.size,subscribeToOtherEmitter(o){let r=o.subscribe(()=>{this.emit()});s.push(r)}}}var Z=Symbol("_");function D(e){let t=[];function n(){if(t.length===0)return e;let r=t.at(-1);return r===Z?e:r}function s(r,u){t.push(r);let i=u();return p(i)?(async()=>{try{return await i}finally{t.pop()}})():(t.pop(),i)}function o(r){let u=n();return()=>{t.push(u);let i=r();return p(i)?(async()=>{try{return await i}finally{t.pop()}})():(t.pop(),i)}}return{run:s,use:n,wrap:o}}function x(){let e=new Map,t=new Set,n=performance.now(),s=!1;function o(){let u=performance.now(),i=u-n,{size:c}=t;if(i<.2&&c>0&&c<10){n=u,r();return}s||(s=!0,Promise.resolve().then(()=>{s=!1,n=performance.now(),r()}))}function r(){if(t.size===0)return;let u=new Set;for(let i of t){if(e.has(i.id)){u.add(i.id);let{onResolveItem:c}=e.get(i.id);c&&c(i.value)}t.delete(i)}if(t.size>0){o();return}for(let i of u)e.get(i)?.onFinish()}return{add(u,i){return e.set(u,i),()=>{e.delete(u)}},schedule(u,i){t.add({value:i,id:u}),o()}}}var M=x(),E=D(void 0);function V(e){let t={},n=!1,s=[],o={},r=h(()=>n?t.current:(n=!0,f()),()=>(n=!0,f()));async function u(){S(t,b)&&(o.controller&&o.controller.abort(),M.schedule(i,null))}let i=y(),c=M.add(i,{onFinish(){t.current=f(),r.emit()}}),G={addEmitter(a){let l=a.subscribe(u);s.push(l)},id:i,sub:u};function H(a){let l=I(a,o.controller);return o.controller=l.controller,l.promise?.then(m=>{t.current=m,r.emit()}).catch(m=>{v(m)||(t.current=m,r.emit())}),l.promise}let f=function(){let a=E.run(G,e);if(!p(a))return t.current=a,a;let l=E.wrap(()=>H(a))();p(l)&&l.catch(()=>null);let m=l;return t.current=m,m};return f.emitter=r,f.destroy=function(){for(let a of s)a();r.clear(),c()},f.id=i,f.listen=function(a){return r.subscribe(()=>{let l=t.current;if(d(l))throw new Error("The value is undefined");a(l)})},f.abort=function(){o.controller&&o.controller.abort()},f}var L=x();function z(e,t=b){let n={};function s(){return d(n.current)&&(n.current=k(e)?e():e),n.current}function o(i){let c=s();n.current=O(i)?i(c):i}let r=function(){let i=s(),c=E.use();return c&&!r.emitter.contains(c.sub)&&c.addEmitter(r.emitter),i};r.listen=function(i){return r.emitter.subscribe(()=>{let c=n.current;if(d(c))throw new Error("The value is undefined");i(c)})},r.emitter=h(()=>r()),r.id=y();let u=L.add(r.id,{onFinish(){n.current=s(),S(n,t)&&r.emitter.emit()},onResolveItem:o});return r.set=function(i){L.schedule(r.id,i)},r.destroy=function(){n.current=void 0,s(),u(),r.emitter.clear()},r.withName=function(i){return r.stateName=i,r},r}var T=require("react");var N=require("react");var w=new WeakMap,_=0;function B(){_++}function U(e){return _=0,{call(){let t=w.get(e);if(t)return t.count++,t.returnType;B();let s={count:1,returnType:V(e)};return w.set(e,s),s.returnType},destroy(){let t=w.get(e);t&&(t.count--,t.count===0&&(t.returnType.destroy(),w.delete(e)))}}}function j(e,t=F){let n=U(e),s=n.call(),o=s.emitter.getInitialSnapshot??s.emitter.getSnapshot;(0,T.useEffect)(()=>n.destroy,[e,n.destroy]);let r=(0,N.useSyncExternalStore)(s.emitter.subscribe,()=>t(s.emitter.getSnapshot()),()=>t(o()));if((0,T.useDebugValue)(r),p(r))throw r;if(A(r))throw n.destroy(),r;return r}function q(e,t){if(e==t)return!0;if(typeof e!="object"||e==null||typeof t!="object"||t==null)return!1;if(P(e)&&P(t)){if(e.size!==t.size)return!1;for(let[o,r]of e)if(!Object.is(r,t.get(o)))return!1;return!0}if(g(e)&&g(t)){if(e.size!==t.size)return!1;for(let o of e)if(!t.has(o))return!1;return!0}if(R(e)&&R(t)){if(e.length!==t.length)return!1;for(let[o,r]of e.entries())if(!Object.is(r,t[o]))return!1;return!0}let n=Object.keys(e),s=Object.keys(t);if(n.length!==s.length)return!1;for(let o of n)if(!Object.prototype.hasOwnProperty.call(t,o)||!Object.is(e[o],t[o]))return!1;return!0}
|
package/esm/create.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{canUpdate as
|
|
1
|
+
import{canUpdate as l,generateId as f}from"./utils/common";import{createEmitter as d}from"./utils/create-emitter";import{isEqualBase as T,isFunction as p,isSetValueFunction as S,isUndefined as o}from"./utils/is";import{context as h}from"./subscriber";import{createGlobalScheduler as w}from"./utils/global-scheduler";const u=w();function R(a,c=T){const r={};function i(){return o(r.current)&&(r.current=p(a)?a():a),r.current}function s(t){const n=i();r.current=S(t)?t(n):t}const e=function(){const t=i(),n=h.use();return n&&!e.emitter.contains(n.sub)&&n.addEmitter(e.emitter),t};e.listen=function(t){return e.emitter.subscribe(()=>{const n=r.current;if(o(n))throw new Error("The value is undefined");t(n)})},e.emitter=d(()=>e()),e.id=f();const m=u.add(e.id,{onFinish(){r.current=i(),l(r,c)&&e.emitter.emit()},onResolveItem:s});return e.set=function(t){u.schedule(e.id,t)},e.destroy=function(){r.current=void 0,i(),m(),e.emitter.clear()},e.withName=function(t){return e.stateName=t,e},e}export{R as create,u as createScheduler};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isPromise as r}from"../utils/is";const t=window?.__REDUX_DEVTOOLS_EXTENSION__?.connect({name:"CustomState",trace:!0});t&&t.init({message:"Initial state"});function a(n){if(!t)return;const{message:o,type:e,value:s,name:i}=n;r(s)||t.send(i,{value:s,type:e,message:o},e)}function m(n,o){return e=>{a({name:n,type:o,value:e,message:"update"})}}export{m as developmentToolsListener,a as sendToDevelopmentTools};
|
package/esm/subscriber.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cancelablePromise as
|
|
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
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
|
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};
|
|
@@ -0,0 +1 @@
|
|
|
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/is.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Abort as
|
|
1
|
+
import{Abort as t}from"./common";function i(n){return n instanceof Promise}function r(n){return typeof n=="function"}function u(n){return n instanceof Map}function s(n){return n instanceof Set}function a(n){return Array.isArray(n)}function f(n,e){return n===e?!0:!!Object.is(n,e)}function c(n){return typeof n=="function"}function p(n){return n instanceof DOMException&&n.name===t.Error}function k(n){return n instanceof Error&&n.name!==t.Error}function w(n){return n===void 0}function x(n){return r(n)&&n.set!==void 0}export{p as isAbortError,k as isAnyOtherError,a as isArray,x as isCreate,f as isEqualBase,r as isFunction,u as isMap,i as isPromise,s as isSet,c as isSetValueFunction,w as isUndefined};
|
package/esm/utils/scheduler.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
const
|
|
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
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{subscriber as c}from"../subscriber";const
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This is not optimal, so for now just ignore. Its just for view and compare if the state is at least similar to others
|
|
3
|
+
* but this tests are not consider as a real benchmark
|
|
4
|
+
*/
|
|
1
5
|
/* eslint-disable unicorn/consistent-function-scoping */
|
|
2
6
|
/* eslint-disable no-console */
|
|
7
|
+
|
|
3
8
|
import { act, renderHook } from '@testing-library/react-hooks'
|
|
4
9
|
import { useStore, create as zustand } from 'zustand'
|
|
5
10
|
import { useEffect, useState } from 'react'
|
|
@@ -33,7 +38,7 @@ describe('benchmarks comparison measure', () => {
|
|
|
33
38
|
beforeEach(() => {
|
|
34
39
|
jest.clearAllMocks()
|
|
35
40
|
})
|
|
36
|
-
const counts = [
|
|
41
|
+
const counts = [10_000]
|
|
37
42
|
for (const count of counts) {
|
|
38
43
|
describe(`Count ${count}`, () => {
|
|
39
44
|
it(`should benchmark ${count} muya first run - idk slow`, async () => {
|
|
@@ -0,0 +1,89 @@
|
|
|
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
|
+
})
|
|
@@ -3,7 +3,7 @@ import { renderHook } from '@testing-library/react-hooks'
|
|
|
3
3
|
import { create } from '../create'
|
|
4
4
|
import { use } from '../use'
|
|
5
5
|
import { Suspense } from 'react'
|
|
6
|
-
import {
|
|
6
|
+
import { waitFor } from '@testing-library/react'
|
|
7
7
|
|
|
8
8
|
describe('use-create', () => {
|
|
9
9
|
const reRendersBefore = jest.fn()
|
|
@@ -26,7 +26,7 @@ describe('use-create', () => {
|
|
|
26
26
|
return <div>Loading...</div>
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const { result
|
|
29
|
+
const { result } = renderHook(
|
|
30
30
|
() => {
|
|
31
31
|
reRendersBefore()
|
|
32
32
|
const data = use(getDataWithUser)
|
|
@@ -36,8 +36,9 @@ describe('use-create', () => {
|
|
|
36
36
|
{ wrapper: ({ children }) => <Suspense fallback={<Loading />}>{children}</Suspense> },
|
|
37
37
|
)
|
|
38
38
|
|
|
39
|
-
await
|
|
40
|
-
|
|
39
|
+
await waitFor(() => {
|
|
40
|
+
expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
|
|
41
|
+
})
|
|
41
42
|
expect(suspenseFunction).toHaveBeenCalledTimes(1)
|
|
42
43
|
expect(result.current).toEqual({ userId: 1, id: 1, title: 'delectus aut autem', completed: false, age: 30 })
|
|
43
44
|
})
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
})
|
|
@@ -2,16 +2,21 @@ import { canUpdate, generateId } from './utils/common'
|
|
|
2
2
|
import type { Emitter } from './utils/create-emitter'
|
|
3
3
|
import { createEmitter } from './utils/create-emitter'
|
|
4
4
|
import { isEqualBase, isFunction, isSetValueFunction, isUndefined } from './utils/is'
|
|
5
|
-
import { createScheduler } from './utils/scheduler'
|
|
5
|
+
// import { createScheduler } from './utils/scheduler'
|
|
6
6
|
import type { Cache, Callable, DefaultValue, IsEqual, Listener, SetValue } from './types'
|
|
7
7
|
import { context } from './subscriber'
|
|
8
|
+
import { createGlobalScheduler } from './utils/global-scheduler'
|
|
8
9
|
|
|
10
|
+
export const createScheduler = createGlobalScheduler()
|
|
9
11
|
interface RawState<T> {
|
|
10
12
|
(): T
|
|
11
13
|
id: number
|
|
12
14
|
set: (value: SetValue<T>) => void
|
|
13
15
|
emitter: Emitter<T>
|
|
14
16
|
listen: Listener<T>
|
|
17
|
+
destroy: () => void
|
|
18
|
+
withName: (name: string) => RawState<T>
|
|
19
|
+
stateName?: string
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export type State<T> = {
|
|
@@ -32,20 +37,21 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
|
|
|
32
37
|
cache.current = isSetValueFunction(value) ? value(previous) : value
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
const schedule = createScheduler<SetValue<T>>({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
})
|
|
40
|
+
// const schedule = createScheduler<SetValue<T>>({
|
|
41
|
+
// onFinish() {
|
|
42
|
+
// cache.current = getValue()
|
|
43
|
+
// if (!canUpdate(cache, isEqual)) {
|
|
44
|
+
// return
|
|
45
|
+
// }
|
|
46
|
+
// state.emitter.emit()
|
|
47
|
+
// },
|
|
48
|
+
// onResolveItem: resolveValue,
|
|
49
|
+
// })
|
|
45
50
|
|
|
46
51
|
const state: RawState<T> = function () {
|
|
47
52
|
const stateValue = getValue()
|
|
48
53
|
const ctx = context.use()
|
|
54
|
+
// console.log('CTX', ctx?.id, 'STATE', state.id)
|
|
49
55
|
if (ctx && !state.emitter.contains(ctx.sub)) {
|
|
50
56
|
ctx.addEmitter(state.emitter)
|
|
51
57
|
}
|
|
@@ -62,6 +68,31 @@ export function create<T>(initialValue: DefaultValue<T>, isEqual: IsEqual<T> = i
|
|
|
62
68
|
}
|
|
63
69
|
state.emitter = createEmitter<T>(() => state())
|
|
64
70
|
state.id = generateId()
|
|
65
|
-
|
|
71
|
+
|
|
72
|
+
const clearScheduler = createScheduler.add(state.id, {
|
|
73
|
+
onFinish() {
|
|
74
|
+
cache.current = getValue()
|
|
75
|
+
if (!canUpdate(cache, isEqual)) {
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
state.emitter.emit()
|
|
79
|
+
},
|
|
80
|
+
onResolveItem: resolveValue,
|
|
81
|
+
})
|
|
82
|
+
state.set = function (value) {
|
|
83
|
+
createScheduler.schedule(state.id, value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
state.destroy = function () {
|
|
87
|
+
cache.current = undefined
|
|
88
|
+
getValue()
|
|
89
|
+
clearScheduler()
|
|
90
|
+
state.emitter.clear()
|
|
91
|
+
}
|
|
92
|
+
state.withName = function (name: string) {
|
|
93
|
+
state.stateName = name
|
|
94
|
+
return state
|
|
95
|
+
}
|
|
96
|
+
|
|
66
97
|
return state
|
|
67
98
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isPromise } from '../utils/is'
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
4
|
+
// @ts-expect-error
|
|
5
|
+
const reduxDevelopmentTools = window?.__REDUX_DEVTOOLS_EXTENSION__?.connect({
|
|
6
|
+
name: 'CustomState', // This will name your instance in the DevTools
|
|
7
|
+
trace: true, // Enables trace if needed
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
if (reduxDevelopmentTools) {
|
|
11
|
+
reduxDevelopmentTools.init({ message: 'Initial state' })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type StateType = 'state' | 'derived'
|
|
15
|
+
|
|
16
|
+
interface SendOptions {
|
|
17
|
+
message?: string
|
|
18
|
+
type: StateType
|
|
19
|
+
value: unknown
|
|
20
|
+
name: string
|
|
21
|
+
}
|
|
22
|
+
export function sendToDevelopmentTools(options: SendOptions) {
|
|
23
|
+
if (!reduxDevelopmentTools) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
const { message, type, value, name } = options
|
|
27
|
+
if (isPromise(value)) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
reduxDevelopmentTools.send(name, { value, type, message }, type)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function developmentToolsListener(name: string, type: StateType) {
|
|
34
|
+
return (value: unknown) => {
|
|
35
|
+
sendToDevelopmentTools({ name, type, value, message: 'update' })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -4,8 +4,12 @@ import { cancelablePromise, canUpdate, generateId } from './utils/common'
|
|
|
4
4
|
import { createContext } from './utils/create-context'
|
|
5
5
|
import type { Emitter } from './utils/create-emitter'
|
|
6
6
|
import { createEmitter } from './utils/create-emitter'
|
|
7
|
-
import {
|
|
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'
|
|
8
11
|
|
|
12
|
+
const subscriberScheduler = createGlobalScheduler()
|
|
9
13
|
interface SubscribeContext<T = unknown> {
|
|
10
14
|
addEmitter: (emitter: Emitter<T>) => void
|
|
11
15
|
id: number
|
|
@@ -29,11 +33,11 @@ export const context = createContext<SubscribeContext | undefined>(undefined)
|
|
|
29
33
|
export function subscriber<F extends AnyFunction, T extends ReturnType<F> = ReturnType<F>>(
|
|
30
34
|
anyFunction: () => T,
|
|
31
35
|
): Subscribe<F, T> {
|
|
36
|
+
const cache: Cache<T> = {}
|
|
37
|
+
let isInitialized = false
|
|
32
38
|
const cleaners: Array<() => void> = []
|
|
33
39
|
const promiseData: CancelablePromise<T> = {}
|
|
34
40
|
|
|
35
|
-
const cache: Cache<T> = {}
|
|
36
|
-
let isInitialized = false
|
|
37
41
|
const emitter = createEmitter(
|
|
38
42
|
() => {
|
|
39
43
|
if (!isInitialized) {
|
|
@@ -56,10 +60,17 @@ export function subscriber<F extends AnyFunction, T extends ReturnType<F> = Retu
|
|
|
56
60
|
promiseData.controller.abort()
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
emitter.emit()
|
|
63
|
+
subscriberScheduler.schedule(id, null)
|
|
61
64
|
}
|
|
65
|
+
|
|
62
66
|
const id = generateId()
|
|
67
|
+
|
|
68
|
+
const clearScheduler = subscriberScheduler.add(id, {
|
|
69
|
+
onFinish() {
|
|
70
|
+
cache.current = subscribe()
|
|
71
|
+
emitter.emit()
|
|
72
|
+
},
|
|
73
|
+
})
|
|
63
74
|
const ctx: SubscribeContext = {
|
|
64
75
|
addEmitter(stateEmitter) {
|
|
65
76
|
const clean = stateEmitter.subscribe(sub)
|
|
@@ -69,30 +80,40 @@ export function subscriber<F extends AnyFunction, T extends ReturnType<F> = Retu
|
|
|
69
80
|
sub,
|
|
70
81
|
}
|
|
71
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
|
+
|
|
72
101
|
const subscribe = function (): T {
|
|
73
102
|
const resultValue = context.run(ctx, anyFunction)
|
|
74
103
|
|
|
75
|
-
if (isPromise(resultValue)) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
promiseWithSelector
|
|
79
|
-
?.then((value) => {
|
|
80
|
-
cache.current = value
|
|
81
|
-
emitter.emit()
|
|
82
|
-
})
|
|
83
|
-
.catch((error) => {
|
|
84
|
-
if (isAbortError(error)) {
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
throw error
|
|
89
|
-
})
|
|
90
|
-
const promiseResult = promiseWithSelector as T
|
|
91
|
-
cache.current = promiseResult
|
|
92
|
-
return promiseResult
|
|
104
|
+
if (!isPromise(resultValue)) {
|
|
105
|
+
cache.current = resultValue
|
|
106
|
+
return resultValue
|
|
93
107
|
}
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
subscribe.emitter = emitter
|
|
@@ -101,6 +122,7 @@ export function subscriber<F extends AnyFunction, T extends ReturnType<F> = Retu
|
|
|
101
122
|
cleaner()
|
|
102
123
|
}
|
|
103
124
|
emitter.clear()
|
|
125
|
+
clearScheduler()
|
|
104
126
|
}
|
|
105
127
|
subscribe.id = id
|
|
106
128
|
subscribe.listen = function (listener: (value: T) => void) {
|
|
@@ -112,6 +134,28 @@ export function subscriber<F extends AnyFunction, T extends ReturnType<F> = Retu
|
|
|
112
134
|
listener(final)
|
|
113
135
|
})
|
|
114
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
|
+
}
|
|
115
159
|
subscribe.abort = function () {
|
|
116
160
|
if (promiseData.controller) {
|
|
117
161
|
promiseData.controller.abort()
|
|
@@ -37,10 +37,8 @@ export function use<F extends AnyFunction, T extends ReturnType<F>, S extends Re
|
|
|
37
37
|
const sub = memo.call()
|
|
38
38
|
const initialSnapshot = sub.emitter.getInitialSnapshot ?? sub.emitter.getSnapshot
|
|
39
39
|
useEffect(() => {
|
|
40
|
-
// memo.call()
|
|
41
40
|
return memo.destroy
|
|
42
|
-
|
|
43
|
-
}, [anyFunction])
|
|
41
|
+
}, [anyFunction, memo.destroy])
|
|
44
42
|
|
|
45
43
|
const value = useSyncExternalStore<S>(
|
|
46
44
|
sub.emitter.subscribe,
|
|
@@ -20,12 +20,14 @@ export interface Emitter<T, P = undefined> {
|
|
|
20
20
|
*/
|
|
21
21
|
export function createEmitter<T, P = undefined>(getSnapshot: () => T, getInitialSnapshot?: () => T): Emitter<T, P> {
|
|
22
22
|
const listeners = new Set<(...params: P[]) => void>()
|
|
23
|
+
// const listeners = new WeakSet<(...params: P[]) => void>()
|
|
23
24
|
const otherCleaners: Array<() => void> = []
|
|
24
25
|
return {
|
|
25
26
|
clear: () => {
|
|
26
27
|
for (const cleaner of otherCleaners) {
|
|
27
28
|
cleaner()
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
listeners.clear()
|
|
30
32
|
},
|
|
31
33
|
subscribe: (listener) => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SchedulerOptions } from './scheduler'
|
|
2
|
+
import { RESCHEDULE_COUNT, THRESHOLD, THRESHOLD_ITEMS } from './scheduler'
|
|
3
|
+
|
|
4
|
+
interface GlobalSchedulerItem<T> {
|
|
5
|
+
value: T
|
|
6
|
+
id: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createGlobalScheduler() {
|
|
10
|
+
const listeners = new Map<number, SchedulerOptions<unknown>>()
|
|
11
|
+
const batches = new Set<GlobalSchedulerItem<unknown>>()
|
|
12
|
+
|
|
13
|
+
let frame = performance.now()
|
|
14
|
+
let scheduled = false
|
|
15
|
+
|
|
16
|
+
function schedule() {
|
|
17
|
+
const startFrame = performance.now()
|
|
18
|
+
const frameSizeDiffIn = startFrame - frame
|
|
19
|
+
const { size } = batches
|
|
20
|
+
if (frameSizeDiffIn < THRESHOLD && size > 0 && size < THRESHOLD_ITEMS) {
|
|
21
|
+
frame = startFrame
|
|
22
|
+
flush()
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!scheduled) {
|
|
27
|
+
scheduled = true
|
|
28
|
+
Promise.resolve().then(() => {
|
|
29
|
+
scheduled = false
|
|
30
|
+
frame = performance.now()
|
|
31
|
+
flush()
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function flush() {
|
|
37
|
+
if (batches.size === 0) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const effectedListeners = new Set<number>()
|
|
42
|
+
for (const value of batches) {
|
|
43
|
+
if (listeners.has(value.id)) {
|
|
44
|
+
effectedListeners.add(value.id)
|
|
45
|
+
const { onResolveItem } = listeners.get(value.id)!
|
|
46
|
+
if (onResolveItem) {
|
|
47
|
+
onResolveItem(value.value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
batches.delete(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (batches.size > RESCHEDULE_COUNT) {
|
|
54
|
+
schedule()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const id of effectedListeners) {
|
|
59
|
+
listeners.get(id)?.onFinish()
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
add<T>(id: number, option: SchedulerOptions<T>) {
|
|
65
|
+
listeners.set(id, option as SchedulerOptions<unknown>)
|
|
66
|
+
return () => {
|
|
67
|
+
listeners.delete(id)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
schedule<T>(id: number, value: T) {
|
|
71
|
+
batches.add({ value, id })
|
|
72
|
+
schedule()
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Abort } from './common'
|
|
2
2
|
import type { SetStateCb, SetValue } from '../types'
|
|
3
|
+
import type { State } from '../create'
|
|
3
4
|
|
|
4
5
|
export function isPromise<T>(value: unknown): value is Promise<T> {
|
|
5
6
|
return value instanceof Promise
|
|
@@ -41,3 +42,9 @@ export function isAnyOtherError(value: unknown): value is Error {
|
|
|
41
42
|
export function isUndefined(value: unknown): value is undefined {
|
|
42
43
|
return value === undefined
|
|
43
44
|
}
|
|
45
|
+
|
|
46
|
+
export function isCreate(value: unknown): value is State<unknown> {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
48
|
+
// @ts-expect-error
|
|
49
|
+
return isFunction(value) && value.set !== undefined
|
|
50
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const THRESHOLD = 0.2
|
|
2
|
-
const THRESHOLD_ITEMS = 10
|
|
3
|
-
const RESCHEDULE_COUNT = 0
|
|
1
|
+
export const THRESHOLD = 0.2
|
|
2
|
+
export const THRESHOLD_ITEMS = 10
|
|
3
|
+
export const RESCHEDULE_COUNT = 0
|
|
4
4
|
|
|
5
|
-
interface
|
|
5
|
+
export interface SchedulerOptions<T> {
|
|
6
6
|
readonly onResolveItem?: (item: T) => void
|
|
7
7
|
readonly onFinish: () => void
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function createScheduler<T>(options:
|
|
10
|
+
export function createScheduler<T>(options: SchedulerOptions<T>) {
|
|
11
11
|
const batches = new Set<T>()
|
|
12
12
|
const { onResolveItem, onFinish } = options
|
|
13
13
|
let frame = performance.now()
|
|
@@ -8,15 +8,27 @@ interface CacheItem<T extends AnyFunction> {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
const cache = new WeakMap<AnyFunction, CacheItem<AnyFunction>>()
|
|
11
|
+
let cacheCount = 0
|
|
12
|
+
|
|
13
|
+
export function getDebugCacheCreation() {
|
|
14
|
+
return cacheCount
|
|
15
|
+
}
|
|
16
|
+
function incrementDebugFunctionCreationCount() {
|
|
17
|
+
cacheCount++
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
export function subMemo<F extends AnyFunction>(anyFunction: F) {
|
|
21
|
+
cacheCount = 0
|
|
13
22
|
return {
|
|
14
23
|
call(): Subscribe<F> {
|
|
15
24
|
const item = cache.get(anyFunction)
|
|
16
25
|
if (item) {
|
|
17
26
|
item.count++
|
|
27
|
+
|
|
18
28
|
return item.returnType
|
|
19
29
|
}
|
|
30
|
+
|
|
31
|
+
incrementDebugFunctionCreationCount()
|
|
20
32
|
const returnType = subscriber(anyFunction)
|
|
21
33
|
const newItem = { count: 1, returnType }
|
|
22
34
|
cache.set(anyFunction, newItem)
|
package/types/create.d.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import type { Emitter } from './utils/create-emitter';
|
|
2
2
|
import type { Callable, DefaultValue, IsEqual, Listener, SetValue } from './types';
|
|
3
|
+
export declare const createScheduler: {
|
|
4
|
+
add<T>(id: number, option: import("./utils/scheduler").SchedulerOptions<T>): () => void;
|
|
5
|
+
schedule<T>(id: number, value: T): void;
|
|
6
|
+
};
|
|
3
7
|
interface RawState<T> {
|
|
4
8
|
(): T;
|
|
5
9
|
id: number;
|
|
6
10
|
set: (value: SetValue<T>) => void;
|
|
7
11
|
emitter: Emitter<T>;
|
|
8
12
|
listen: Listener<T>;
|
|
13
|
+
destroy: () => void;
|
|
14
|
+
withName: (name: string) => RawState<T>;
|
|
15
|
+
stateName?: string;
|
|
9
16
|
}
|
|
10
17
|
export type State<T> = {
|
|
11
18
|
readonly [K in keyof RawState<T>]: RawState<T>[K];
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type StateType = 'state' | 'derived';
|
|
2
|
+
interface SendOptions {
|
|
3
|
+
message?: string;
|
|
4
|
+
type: StateType;
|
|
5
|
+
value: unknown;
|
|
6
|
+
name: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function sendToDevelopmentTools(options: SendOptions): void;
|
|
9
|
+
export declare function developmentToolsListener(name: string, type: StateType): (value: unknown) => void;
|
|
10
|
+
export {};
|
package/types/utils/is.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SetStateCb, SetValue } from '../types';
|
|
2
|
+
import type { State } from '../create';
|
|
2
3
|
export declare function isPromise<T>(value: unknown): value is Promise<T>;
|
|
3
4
|
export declare function isFunction<T extends (...args: unknown[]) => unknown>(value: unknown): value is T;
|
|
4
5
|
export declare function isMap(value: unknown): value is Map<unknown, unknown>;
|
|
@@ -9,3 +10,4 @@ export declare function isSetValueFunction<T>(value: SetValue<T>): value is SetS
|
|
|
9
10
|
export declare function isAbortError(value: unknown): value is DOMException;
|
|
10
11
|
export declare function isAnyOtherError(value: unknown): value is Error;
|
|
11
12
|
export declare function isUndefined(value: unknown): value is undefined;
|
|
13
|
+
export declare function isCreate(value: unknown): value is State<unknown>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
export declare const THRESHOLD = 0.2;
|
|
2
|
+
export declare const THRESHOLD_ITEMS = 10;
|
|
3
|
+
export declare const RESCHEDULE_COUNT = 0;
|
|
4
|
+
export interface SchedulerOptions<T> {
|
|
2
5
|
readonly onResolveItem?: (item: T) => void;
|
|
3
6
|
readonly onFinish: () => void;
|
|
4
7
|
}
|
|
5
|
-
export declare function createScheduler<T>(options:
|
|
6
|
-
export {};
|
|
8
|
+
export declare function createScheduler<T>(options: SchedulerOptions<T>): (value: T) => void;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Subscribe } from '../subscriber';
|
|
2
2
|
import type { AnyFunction } from '../types';
|
|
3
|
+
export declare function getDebugCacheCreation(): number;
|
|
3
4
|
export declare function subMemo<F extends AnyFunction>(anyFunction: F): {
|
|
4
5
|
call(): Subscribe<F>;
|
|
5
6
|
destroy(): void;
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-shadow */
|
|
2
|
-
/* eslint-disable no-shadow */
|
|
3
|
-
import { act, renderHook } from '@testing-library/react-hooks'
|
|
4
|
-
import { create } from '../create'
|
|
5
|
-
import { use } from '../use'
|
|
6
|
-
import { subscriber } from '../subscriber'
|
|
7
|
-
import { waitFor } from '@testing-library/react'
|
|
8
|
-
|
|
9
|
-
describe('use-create', () => {
|
|
10
|
-
const reRendersBefore = jest.fn()
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
jest.clearAllMocks()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
it('should test sub hook', async () => {
|
|
17
|
-
const state = create(1)
|
|
18
|
-
|
|
19
|
-
const sub = subscriber(state)
|
|
20
|
-
expect(sub()).toBe(1)
|
|
21
|
-
|
|
22
|
-
act(() => {
|
|
23
|
-
state.set(2)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
await waitFor(() => {})
|
|
27
|
-
expect(sub()).toBe(2)
|
|
28
|
-
})
|
|
29
|
-
it('should test use hook', async () => {
|
|
30
|
-
const state = create(1)
|
|
31
|
-
|
|
32
|
-
const { result, waitFor } = renderHook(() => {
|
|
33
|
-
reRendersBefore()
|
|
34
|
-
return use(state)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
state.set(2)
|
|
38
|
-
|
|
39
|
-
await waitFor(() => {})
|
|
40
|
-
expect(result.current).toBe(2)
|
|
41
|
-
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
42
|
-
|
|
43
|
-
state.set(3)
|
|
44
|
-
|
|
45
|
-
await waitFor(() => {})
|
|
46
|
-
expect(result.current).toBe(3)
|
|
47
|
-
expect(reRendersBefore).toHaveBeenCalledTimes(3)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('should test derived state', async () => {
|
|
51
|
-
const state1 = create(1)
|
|
52
|
-
const state2 = create(2)
|
|
53
|
-
|
|
54
|
-
function derivedBefore(plusValue: number) {
|
|
55
|
-
return state1() + state2() + plusValue
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function derived() {
|
|
59
|
-
return state1() + state2() + derivedBefore(10)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const { result, waitFor } = renderHook(() => {
|
|
63
|
-
reRendersBefore()
|
|
64
|
-
return use(derived)
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
act(() => {
|
|
68
|
-
state1.set(2)
|
|
69
|
-
state2.set(3)
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
await waitFor(() => {})
|
|
73
|
-
expect(result.current).toBe(20)
|
|
74
|
-
expect(reRendersBefore).toHaveBeenCalledTimes(2)
|
|
75
|
-
})
|
|
76
|
-
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|